1668772500
A guide on changing an element's id using jQuery
To change the id
attribute of an HTML element, you can use the jQuery attr()
method which allows you to set an element’s attribute value.
The syntax of attr()
method is as follows:
Element.attr("attributeName", "attributeValue");
In the following example, the <h1>
element with id
value of myId
will be replaced with newId
:
<head>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<h1 id="myId">jQuery change ID value</h1>
<script>
$("#myId").attr("id", "newId");
</script>
</body>
You can change any HTML element id
attribute value by using the example above as a reference.
Original article source at: https://sebhastian.com/
1667288880
A php mapper very tunable, cross-orm and cross-DBMS
Installation
Add to your composer.json:
"require": {
"kassko/data-mapper": "~0.12.5"
}
Note that:
Run tests
./bin/phpunit
Usage
If you use annotations format, register the autoloader:
$loader = require 'vendor/autoload.php';
Doctrine\Common\Annotations\AnnotationRegistry::registerLoader($loader);
And run the environment:
(new Kassko\DataMapper\DataMapperBuilder)->run();
$id = 1;
//Construct a person, set an id to it and implicitly load person from the given id fetching all sources configured.
$person = new Kassko\Sample\Person($id);
echo $person->getName();
//Here some stuff to retrieve the entityManager.
$id = 1;
$person = $entityManager->getRepository('Kassko\Sample\Person')->find($id);
echo $person->getName();
With data-mapper, the access logic is in the configuration in object with annotations (note that the configuration also could be in a separed yaml or php file or in the object with inner php or inner yaml).
Maybe you need to create an object you don't know if it already persisted or new.
$dataMapper = (new Kassko\DataMapper\DataMapperBuilder)->instance();
$id = 1;
$person = new Kassko\Sample\Person()->setId($id);//Only construct a person and set an id to it.
$dataMapper->load($person);//Explicitly load person from the given id fetching all sources configured.
echo $person->getName();
The variant above allows not to load automatically the object if parameters are not send by the constructor but by the setters. If the client code always create already persisted object, the first version is good. If this client code sometimes create new objects (objects that are not already persisted), you should use the second version which will not attempt to load your objects that are not already existing in your storage.
namespace Kassko\Sample;
use Kassko\DataMapper\ObjectExtension\LoadableTrait;
/**
* @DM\RefImplicitSource(id="personSource")
*
* @DM\DataSourcesStore({
* @DM\DataSource(
* id="personSource",
* class="Kassko\Sample\PersonDataSource",
* method="getData",
* args="#id",
* supplySeveralFields=true
* )
* })
*/
class Person
{
use LoadableTrait;
/**
* @DM\ExcludeImplicitSource
*/
private $id;//Not linked to the implicit source because of the annotation ExcludeImplicitSource.
private $firstName;//Linked to the implicit source idem for name, email and phone.
private $name;
private $email;
private $phone;
public function __construct($id = null)
{
if (null !== $id) {
$this->id = $id;
$this->load();//Implicit loading is performed by the constructor not by the setter setId().
}
}
public function getId() { return $this->id;}
public function setId($id) { $this->id = $id; return $this; }
public function getFirstName() { return $this->firstName; }
public function setFirstName($firstName) { $this->firstName = $firstName; return $this; }
public function getName() { return $this->name; }
public function setName($name) { $this->name = $name; return $this; }
public function getEmail() { return $this->email; }
public function setEmail($email) { $this->email = $email; return $this; }
public function getPhone() { return $this->phone; }
public function setPhone($phone) { $this->phone = $phone; return $this; }
}
namespace Kassko\Sample;
class PersonDataSource
{
public function getData($id)
{
$data = $this->connection->executeQuery('select first_name as firstName, name, email, phone from some_table where id = ?', [$id]);
if (! isset($data[0])) {
return null;
}
return $data[0];
}
}
id
. An arbitrary id for the source. Optional but necessary if you need to mentionned the source in another annotation.class
. The class of the source that return datas. If the source has some dependencies (above PersonDataSource
has a dependency $connection
), its instanciation can be performed by a resolver named class-resolver. See more details here.method
. The name of the method that return datas.args
. Arguments of the method that return datas. You can send a raw value, a field value with the prefix #
(ex: #id
), an expression and more ... See more details heresupplySeveralFields
. Whether the source returns the data directly or put them in a key of an array and return the array. If the source supply only one field, it can return directly the data, these data will be bound to the good property. Else the source should return an array with as keys as property to supply. The key is named like the property or a mapping is done in the annotation Field. ```php namespace Kassko\Sample;class Person { private $name;
/**
* @DM\Field(name="first_name")
*/
private $firstName;
}
```php
namespace Kassko\Sample;
class PersonDataSource
{
public function getData($id)
{
$data = $this->connection->executeQuery('select name, first_name from some_table where id = ?', [$id]);
if (! isset($data[0])) {
return null;
}
return $data[0];
}
}
Data-mapper is not an ORM so it cannot generate for you some sql statement. But you can use it with an ORM like Doctrine ORM.
namespace Kassko\Sample;
use Kassko\DataMapper\ObjectExtension\LoadableTrait;
/**
* @DM\RefImplicitSource(id="personSource")
*
* @DM\DataSourcesStore({
* @DM\DataSource(
* id="personSource",
* class="Kassko\Sample\PersonDataSource",
* method="getData",
* args="#id",
* supplySeveralFields=true
* ),
* @DM\DataSource(
* id="carSource",
* class="Kassko\Sample\CarRepository",
* method="find",
* args="expr(source('personSource')['car_id'])"
* )
* })
*
*
* @DM\RefImplicitSource(id="personSource")
*/
class Person
{
use LoadableTrait;
/**
* @DM\ExcludeImplicitSource
*/
private $id;
private $firstName;
private $name;
private $email;
private $phone;
/**
* @DM\RefSource(id="carSource")
*/
private $car;
public function __construct($id = null)
{
if (null !== $id) {
$this->id = $id;
$this->load();//Implicit loading is performed by the constructor not by the setter setId().
}
}
public function getId() { return $this->id;}
public function setId($id) { $this->id = $id; return $this; }
public function getFirstName() { return $this->firstName; }
public function setFirstName($firstName) { $this->firstName = $firstName; return $this; }
public function getName() { return $this->name; }
public function setName($name) { $this->name = $name; return $this; }
public function getEmail() { return $this->email; }
public function setEmail($email) { $this->email = $email; return $this; }
public function getPhone() { return $this->phone; }
public function setPhone($phone) { $this->phone = $phone; return $this; }
public function getCar();
public function setCar($car) { $this->car = $car; return $this; }
}
namespace Kassko\Sample;
class PersonDataSource
{
private $connection;
public function __construct($connection)
{
$this->connection = $connection;
}
public function getData($id)
{
$data = $this->connection->executeQuery('select firstName, name, email, phone, car_id from some_table where id = ?', [$id]);
if (! isset($data[0])) {
return null;
}
return $data[0];
}
}
namespace Kassko\Sample;
use Doctrine\ORM\EntityRepository;
/**
* CarRepository is a Doctrine source that feed the property $car.
*/
class CarRepository extends EntityRepository
{
}
CarRepository has some dependencies too (the entity manager), it is instantiated with a resolver class-resolver. Reminder: you can see more details here.
Run environment with class-resolver:
(new Kassko\DataMapper\DataMapperBuilder)
->settings(['class_resolver' => $classResolver])
->run()
;
But before, create your class-resolver (example of basic resolver):
$classResolver = function ($class, & $supported = true) {
if ('Kassko\Sample\PersonDataSource' === $class) {
return new Kassko\Sample\PersonDataSource(new Kassko\Sample\Connection);
}
return null;
};
The settings key class_resolver
accepts either a callable (a closure / an array like [$class, $method]
/ an invocable class) or an instance of Kassko\ClassResolver\ClassResolverInterface
.
You can see more details about class-resolver
here.
Features: details
=======================================================================================================
Given an object:
namespace Kassko\Sample;
class Watch
{
private $brand;
private $color;
public function getBrand() { return $this->brand; }
public function setBrand($brand) { $this->brand = $brand; }
public function getColor() { return $this->color; }
public function setColor($color) { $this->color = $color; }
}
Use the ResultBuilder:
The result builder allows you to hydrate all a collection from a result that contains several records. It also allows you to get only one result even if the result contains several records.
$data = [
0 => [
'brand' => 'some brand',
'color' => 'blue',
],
1 => [
'brand' => 'some other brand',
'color' => 'green',
],
];
$dataMapper = (new Kassko\DataMapper\DataMapperBuilder)->instance();
$dataMapper->resultBuilder('Kassko\Sample\Watch', $data)->all();//The result will be an array with two objects.
$dataMapper->resultBuilder('Kassko\Sample\Watch', $data)->first();//The result will be a watch object representing the first record.
The code above will display:
array(2) {
["brand"]=>
string(10) "some brand"
["color"]=>
string(4) "blue"
}
Inversely, you can extract values of an object or of an object collection:
$dataMapper = (new Kassko\DataMapper\DataMapperBuilder)->instance();
$dataMapper->resultBuilder('Kassko\Sample\Watch')->raw($object);
$dataMapper->resultBuilder('Kassko\Sample\Watch')->raw($collection);
Above, we use the default hydrator. But you often need to customize hydration because column names are different of properties name and for many other reasons.
To customize the hydration you should provide a mapping configuration. Severals formats are available but in this documentation we choose to use the annotation format (more details about all mapping configuration format are available here
).
namespace Kassko\Sample;
use Kassko\DataMapper\Annotation as DM;
class Watch
{
private $brand;
/**
* @DM\Fields(name="COLOR")
*/
private $color;
public function getBrand() { return $this->brand; }
public function setBrand($brand) { $this->brand = $brand; }
public function getColor() { return $this->color; }
public function setColor($color) { $this->color = $color; }
}
$loader = require 'vendor/autoload.php';
Doctrine\Common\Annotations\AnnotationRegistry::registerLoader(array($loader, 'loadClass'));
$dataMapper = (new Kassko\DataMapper\DataMapperBuilder)->instance();
$dataMapper->resultBuilder('Kassko\Sample\Watch', $data)->all();
You can find below all the ways to get results with the ResultBuilder:
/*
Return an array of objects.
So return an array with only one object, if only one fullfill the request.
*/
$resultBuilder->all();
/*
Return the object found.
If more than one result are found, throw an exception
Kassko\DataMapper\Result\Exception\NonUniqueResultException.
If no result found, throw an exception
Kassko\DataMapper\Result\Exception\NoResultException.
*/
$resultBuilder->single();
/*
Return the object found or null.
*/
$resultBuilder->one();
/*
Return the object found or a default result (like false).
*/
$resultBuilder->one(false);
/*
If more than one result are found, throw an exception
Kassko\DataMapper\Result\Exception\NonUniqueResultException.
*/
/*
Return the first object found or null.
*/
$resultBuilder->first();
/*
Return the first object found or a default result (like value false).
*/
$resultBuilder->first(false);
/*
If no result found, throw an exception
Kassko\DataMapper\Result\Exception\NoResultException.
*/
/*
Return an array indexed by a property value.
If the index does not exists (allIndexedByUnknown()), throw an exception Kassko\DataMapper\Result\Exception\NotFoundIndexException.
If the same index is found twice, throw an exception
Kassko\DataMapper\Result\Exception\DuplicatedIndexException.
Examples:
allIndexedByBrand() will index by brand value:
[
'BrandA' => $theWatchInstanceWithBrandA,
'BrandB' => $theWatchInstanceWithBrandB,
]
allIndexedByColor() will index by color value:
[
'Blue' => $theWatchInstanceWithColorBlue,
'Red' => $theWatchInstanceWithColorRed,
]
allIndexedByUnknown() will throw a Kassko\DataMapper\Result\Exception\NotFoundIndexException.
*/
$resultBuilder->allIndexedByBrand();//Indexed by brand value
//or
$resultBuilder->allIndexedByColor();//Indexed by color value
/*
Return an iterator.
Result will not be hydrated immediately but only when you will iterate the results (with "foreach" for example).
*/
$result = $resultBuilder->iterable();
foreach ($result as $object) {//$object is hydrated
if ($object->getColor() === 'blue') {
break;
//We found the good object then we stop the loop and others objects in results will not be hydrated.
}
}
/*
Return an iterator indexed by a property value.
If the index does not exists, throw an exception Kassko\DataMapper\Result\Exception\NotFoundIndexException.
If the same index is found twice, throw an exception Kassko\DataMapper\Result\Exception\DuplicatedIndexException.
*/
$resultBuilder->iterableIndexedByBrand();
//or
$resultBuilder->iterableIndexedByColor();
This section will be written later.
namespace Kassko\Sample;
use Kassko\DataMapper\Annotation as DM;
use Kassko\DataMapper\Hydrator\HydrationContextInterface;
use Kassko\DataMapper\Hydrator\Value;
use \DateTime;
class Watch
{
private static $brandCodeToLabelMap = [1 => 'Brand A', 2 => 'Brand B'];
private static $brandLabelToCodeMap = ['Brand A' => 1, 'Brand B' => 2];
/**
* @DM\Field(readConverter="readBrand", writeConverter="writeBrand")
*/
private $brand;
/**
* @DM\Field(readConverter="hydrateBool", writeConverter="extractBool")
*/
private $waterProof;
/**
* @DM\Field(readConverter="hydrateBoolFromSymbol", writeConverter="extractBoolToSymbol")
*/
private $stopWatch;
/**
* @DM\Field(type="date", readDateConverter="Y-m-d H:i:s", writeDateConverter="Y-m-d H:i:s")
*/
private $createdDate;
public function getBrand() { return $this->brand; }
public function setBrand($brand) { $this->brand = $brand; }
public function isWaterProof() { return $this->waterProof; }
public function setWaterProof($waterProof) { $this->waterProof = $waterProof; }
public function hasStopWatch() { return $this->stopWatch; }
public function setStopWatch($stopWatch) { $this->stopWatch = $stopWatch; }
public function getCreatedDate() { return $this->createdDate; }
public function setCreatedDate(DateTime $createdDate) { $this->createdDate = $createdDate; }
public static function readBrand(Value $value, HydrationContextInterface $context)
{
if (isset(self::$brandCodeToLabelMap[$value->value])) {
$value->value = self::$brandCodeToLabelMap[$value->value];
}
}
public static function writeBrand(Value $value, HydrationContextInterface $context)
{
if (isset(self::$brandLabelToCodeMap[$value->value])) {
$value->value = self::$brandLabelToCodeMap[$value->value];
}
}
public static function hydrateBool(Value $value, HydrationContextInterface $context)
{
$value->value = $value->value == '1';
}
public static function extractBool(Value $value, HydrationContextInterface $context)
{
$value->value = $value->value ? '1' : '0';
}
public static function hydrateBoolFromSymbol(Value $value, HydrationContextInterface $context)
{
$value->value = $value->value == 'X';
}
public static function extractBoolToSymbol(Value $value, HydrationContextInterface $context)
{
$value->value = $value->value ? 'X' : ' ';
}
}
readDateConverter contains the format of the string to transform into Date object. Internally, DataMapper uses the Php function DateTime::createFromFormat() to create a Date object from a raw string.
writeDateConverter contains the string format in which you want to serialize your Date object. Internally, DataMapper uses the Php function DateTime::createFromFormat() to serialise the Date object in a string.
Given this code:
$data = [
'brand' => '1',
'waterProof' => '1',
'stopWatch' => 'X',
'created_date' => '2014-09-14 12:36:52',
];
$dataMapper = (new Kassko\DataMapper\DataMapperBuilder)->instance();
$object = new Kassko\Sample\Watch;
$dataMapper->hydrator('Kassko\Sample\Watch')->hydrate($data, $object);
var_dump($object);
Your output will be:
object(Watch)#283 (8) {
["brand":"Watch":private]=> string(10) "Brand A"
["waterProof":"Watch":private]=> bool(true)
["stopWatch":"Watch":private]=> bool(true)
["createdDate":"Watch":private]=>
object(DateTime)#320 (3) { ["date"]=> string(19) "2014-09-14 12:36:52" ["timezone_type"]=> int(3) ["timezone"]=> string(13) "Europe/Berlin" }
}
If the created_date had a bad format, an exception would have been thrown. For example, the format given above in the read date converter is 'Y-m-d H:i:s', so a create_date like '2014 09 14 12h36m52s' is not correct.
This section will be written later.
This section will be written later.
DataMapper automatically recognize getter (or isser or haser) and setter of a field.
namespace Kassko\Sample;
use Kassko\DataMapper\Annotation as DM;
class Watch
{
/**
* @DM\Field
*/
private $brand;
/**
* @DM\Field
*/
private $waterProof;
/**
* @DM\Field
*/
private $stopWatch;
public function getBrand() { return $this->brand; }
public function setBrand($brand) { $this->brand = $brand; }
public function isWaterProof() { return $this->waterProof; }
public function setWaterProof($waterProof) { $this->waterProof = $waterProof; }
public function hasStopWatch() { return $this->stopWatch; }
public function setStopWatch($stopWatch) { $this->stopWatch = $stopWatch; }
}
To retrieve the corresponding getter, DataMapper look in order at:
You also can specify corresponding getters/setters:
namespace Kassko\Sample;
use Kassko\DataMapper\Annotation as DM;
class Watch
{
/**
* @DM\Field(prefix="is")
*/
private $waterProof;
/**
* @DM\Getter(prefix="has")
*/
private $alarm;
/**
* @DM\Getter(prefix="are")
*/
private $handsYellow;
/**
* @DM\Field(name="hasStopWatch")
*/
private $stopWatch;
/**
* @DM\Getter(name="canColorChange")
* @DM\Setter(name="colorCanChange")
*/
private $variableColor;
public function isWaterProof() { return $this->waterProof; }
public function setWaterProof($waterProof) { $this->waterProof = $waterProof; }
public function hasAlarm() { return $this->alarm; }
public function setAlarm($stopWatch) { $this->alarm = $alarm; }
public function areHandsYellow() { return $this->handsYellow; }
public function setHandsyellow($handsYellow) { $this->handsYellow = $handsYellow; }
public function hasStopWatch() { return $this->stopWatch; }
public function setStopWatch($stopWatch) { $this->stopWatch = $stopWatch; }
public function canColorChange() { return $this->variableColor; }
public function colorCanChange($colorCanChange) { $this->variableColor = $colorCanChange; }
}
This section will be written later.
This section will be written later.
This section will be written later.
This section will be written later.
This section will be written later.
This section will be written later.
This section will be written later.
You can use the same model with various mapping configuration but you must work with one of the outer mapping configuration and not with mapping embedded in the object. So 'yaml_file' or 'php_file' are correct mapping format but 'annotations', 'inner_php' or 'inner_yaml' are bad format.
namespace Kassko\Sample;
class Color
{
private $red;
private $green;
private $blue;
public function getRed() { return $this->red; }
public function setRed($red) { $this->red = $red; }
public function getGreen() { return $this->green; }
public function setGreen($green) { $this->green = $green; }
public function getBlue() { return $this->blue; }
public function setBlue($blue) { $this->blue = $blue; }
}
A english data source with the mapping in yaml:
# color_en.yml
fields:
red: ~
green: ~
blue: ~
A french data source with the mapping in yaml:
# color_fr.yml
fields:
red:
name: rouge
green:
name: vert
blue:
name: bleu
And imagine we've got a spanish data source with the mapping in a php format.
//color_es.php
return [
'fields' => [
'red' => 'rojo',
'green' => 'verde',
'blue' => 'azul',
],
];
use DataMapper\Configuration\RuntimeConfiguration;
$data = [
'red' => '255',
'green' => '0',
'blue' => '127',
];
$resultBuilder = $dataMapper->resultBuilder('Kassko\Sample\Color', $data);
$resultBuilder->setRuntimeConfiguration(
(new RuntimeConfiguration)
->addClassMetadataDir('Color', 'some_resource_dir')//Optional, if not specified Configuration::defaultClassMetadataResourceDir is used.
->addMappingResourceInfo('Color', 'color_en.yml', 'inner_yaml')
);
$resultBuilder->single();
use DataMapper\Configuration\RuntimeConfiguration;
$data = [
'rouge' => '255',
'vert' => '0',
'bleu' => '127',
];
$resultBuilder = $dataMapper->resultBuilder('Kassko\Sample\Color', $data);
$resultBuilder->setRuntimeConfiguration(
(new RuntimeConfiguration)
->addClassMetadataDir('Color', 'some_resource_dir')
->addMappingResourceInfo('Color', 'color_fr.yml', 'inner_yaml')
);
$resultBuilder->single();
use DataMapper\Configuration\RuntimeConfiguration;
$data = [
'rojo' => '255',
'verde' => '0',
'azul' => '127',
];
$resultBuilder = $dataMapper->resultBuilder('Kassko\Sample\Color', $data);
$resultBuilder->setRuntimeConfiguration(
(new RuntimeConfiguration)
->addClassMetadataDir('Color', 'some_resource_dir')
->addMappingResourceInfo('Color', 'color_es.php', 'inner_php')
);
$resultBuilder->single();
namespace Kassko\Sample;
use Kassko\DataMapper\Annotation as DM;
class Customer
{
/**
* @DM\Field
* @DM\Id
*/
private $id;
/**
* @DM\Field(class="Kassko\Sample\Address")
* @DM\Config(mappingResourceType="yaml", mappingResourceName="billing_address.yml")
*/
private $billingAddress;//$billingAddress is a value object.
/**
* @DM\Field(class="Kassko\Sample\Address")
* @DM\Config(mappingResourceType="yaml", mappingResourceName="shipping_address.yml")
*/
private $shippingAddress;//$shippingAddress is a value object too.
}
namespace Kassko\Sample;
class Address
{
private $street;
private $town;
private $postalCode;
private $country;
}
# billing_address.yml
fields:
street:
name: billing_street
town:
name: billing_town
postalCode:
name: billing_postal_code
country:
name: billing_country
# shipping_address.yml
fields:
street:
name: shipping_street
town:
name: shipping_town
postalCode:
name: shipping_postal_code
country:
name: shipping_country
$data = [
'id' => 1,
'billing_street' => '12 smarties street',
'billing_town' => 'Nuts',
'billing_postal_code' => '654321'
'billing_country' => 'England',
'shipping_street' => '23 smarties street',
'shipping_town' => 'Mars',
'shipping_postal_code' => '987654'
'shipping_country' => 'England',
];
$dataMapper->resultBuilder('Kassko\Sample\Customer', $data)->single();
Note that you can have value objects which contains value objects and so on. And each value object can use it's own mapping configuration format.
namespace Kassko\Sample;
use Kassko\DataMapper\Annotation as DM;
class Information
{
/**
* @DM\DataSource(class="Kassko\Sample\ShopDataSource", method="getBestShop")
* @DM\Field(class='Kassko\Sample\Shop')
*/
private $bestShop;
/**
* @DM\DataSource(class="Kassko\Sample\ShopDataSource", method="getNbShops")
*/
private $nbShops;
public function getBestShop() { return $this->bestShop; }
public function setBestShop(Shop $shop) { $this->bestShop = $bestShop; }
}
namespace Kassko\Sample;
use Kassko\DataMapper\Annotation as DM;
class Shop
{
/**
* @DM\Field
*/
private $name;
/**
* @DM\Field
*/
private $address;
}
namespace Kassko\Sample;
class ShopDataSource
{
public function getBestShop()
{
return [
'name' => 'The best',
'address' => 'Street of the bests',
];
}
public function getNbShops()
{
return 25;
}
}
This section will be written later.
Below, we load properties "bestShop" and "keyboard" only when we use it.
namespace Kassko\Sample;
use Kassko\DataMapper\Annotation as DM;
use Kassko\DataMapper\ObjectExtension\LazyLoadableTrait;
class Information
{
use LazyLoadableTrait;
/**
* @DM\DataSource(class="Kassko\Sample\ShopDataSource", method="getBestShop", lazyLoading=true)
* @DM\Field(class='Kassko\Sample\Shop')
*/
private $bestShop;
/**
* @DM\DataSource(class="Kassko\Sample\ShopDataSource", method="getNbShops", lazyLoading=true)
*/
private $nbShops;
public function getBestShop()
{
$this->loadProperty('bestShop');//<= Load the best shop from the property name if not loaded.
return $this->bestShop;
}
public function getNbShop()
{
$this->loadProperty('nbShops');//<= Load the best shop from the property name if not loaded.
return $this->nbShops;
}
}
namespace Kassko\Sample;
use Kassko\DataMapper\Annotation as DM;
class Shop
{
/**
* @DM\Field
*/
private $name;
/**
* @DM\Field
*/
private $address;
}
namespace Kassko\Sample;
class ShopDataSource
{
public function getBestShop()
{
return [
'name' => 'The best',
'address' => 'Street of the bests',
];
}
public function getNbShops()
{
return 25;
}
}
Given this code:
namespace Kassko\Sample;
use Kassko\DataMapper\Annotation as DM;
class Person
{
/**
* @DM\DataSource(class="Kassko\Sample\PersonDataSource", method="getData", lazyLoading=true, supplySeveralFields=true)
*/
private $name;
/**
* @DM\DataSource(class="Kassko\Sample\PersonDataSource", method="getData", lazyLoading=true, supplySeveralFields=true)
*/
private $address;
/**
* @DM\DataSource(class="Kassko\Sample\PersonDataSource", method="getData", lazyLoading=true, supplySeveralFields=true)
*/
private $phone;
/**
* @DM\DataSource(class="Kassko\Sample\PersonDataSource", method="getData", lazyLoading=true, supplySeveralFields=true)
*/
private $email;
}
namespace Kassko\Sample;
class PersonDataSource
{
public function getData()
{
return [
'name' => 'Foo',
'address' => 'Blue road',
'phone' => '01 02 03 04 05',
'email' => 'foo@bar.baz',
];
}
}
We can remove the duplication:
namespace Kassko\Sample;
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\DataSourcesStore({
* @DM\DataSource(
* id="some_source", class="Kassko\Sample\PersonDataSource", method="getData", lazyLoading=true
* )
* })
*/
class Person
{
/**
* @DM\RefSource(id="some_source")
*/
private $name;
/**
* @DM\RefSource(id="some_source")
*/
private $address;
/**
* @DM\RefSource(id="some_source")
*/
private $phone;
/**
* @DM\RefSource(id="some_source")
*/
private $email;
}
Here, sourceB replace sourceA if its not stable:
namespace Kassko\Sample;
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\DataSourcesStore({
* @DM\DataSource(
* id="sourceA", class="Kassko\Sample\ShopDataSource", method="getData", lazyLoading=true,
* fallbackSourceId="sourceB", onFail="checkException", exceptionClass="Kassko\Sample\NotStableSourceException"
* )
* })
*/
class Person
{
}
Or:
namespace Kassko\Sample;
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\DataSourcesStore({
* @DM\DataSource(
* id="sourceA", class="Kassko\Sample\ShopDataSource", method="getData", lazyLoading=true,
* fallbackSourceId="sourceB", onFail="checkReturnValue", badReturnValue="null"
* )
* })
*/
class Person
{
}
Bad return values could be: "null"
, "false"
, "emptyString"
or "emptyArray"
.
namespace Kassko\Sample;
use Kassko\DataMapper\Annotation as DM;
class Key
{
use LazyLoadableTrait;
private $note;
private $octave = 3;
/**
* @DM\DataSource(
* id="ida",
* class="Kassko\Samples\KeyLLManager",
* method="getData", supplySeveralFields=true,
* preprocessors = @DM\Methods({
* @DM\Method(method="somePreprocessor"),
* @DM\Method(class="Kassko\Sample\KeyProcessor", method="preprocess", args="##this")
* })
* processors = @DM\Methods({
* @DM\Method(method="someProcessor"),
* @DM\Method(class="Kassko\Sample\KeyProcessor", method="process", args="##this")
* })
*)
*/
public $color;
public function getColor()
{
$this->loadProperty('color');
return $this->color;
}
public function somePrepocessor()
{
//Some stuff
}
public function someProcessor())
{
//Some stuff
}
}
namespace Kassko\Sample;
class KeyPreprocessor
{
public function preprocess(Key $key)
{
$this->logger->info(sprintf('Before key hydration %s', $key->getId()));
}
public function process($keyColor)
{
$this->logger->info(sprintf('After key hydration %s', $key->getId()));
}
}
Or:
class Key
{
use LazyLoadableTrait;
private $note;
private $octave = 3;
/**
* @DM\DataSource(
* id="ida",
* class="Kassko\Samples\KeyLLManager",
* method="getData", supplySeveralFields=true,
* preprocessor = @DM\Method(method="somePreprocessor"),
* processor = @DM\Method(method="someProcessor")
*)
*/
public $color;
public function getColor()
{
$this->loadProperty('color');
return $this->color;
}
public function somePrepocessor()
{
//Some stuff
}
public function someProcessor())
{
//Some stuff
}
}
depends
contains sources of which depends an other source. It's usefull when you need to ensures that a property is already available in a zone.
This section will be completed later.
This section will be written later.
This section will be written later.
This section will be written later.
This section will be written later.
This section will be written later.
This section will be written later.
This section will be written later.
This section will be written later.
[
'mapping' =>
[
//Default is "annotations" or other type (1).
'default_resource_type' => 'annotations',
//Optional key.
'default_resource_dir' => 'some_dir',
//Optional key. Has only sense if you use inner_php or inner_yaml format.
//It's the method whereby you provide the mapping.
'default_provider_method' => 'some_method_name',
//Optional section.
'groups' =>
[
'some_group' =>
[
//Default is "annotations" or other type (1).
'resource_type' => annotations,
//The resource dir of the given bundle.
'resource_dir' => 'some_dir',
//Default value is null.
'provider_method' => null,
],
],
//Optional section
'objects':
[
[
//Required (the full qualified object class name).
'class' => 'some_fqcn',
//Optional key. Allows to inherit settings from a group if there are not specified.
'group' => 'some_group',
//Optional key.
'resource_type' => 'yaml_file',
//Optional key.
//The resource directory with the resource name.
//If not defined, data-mapper fallback to resource_name and prepend to it a resource_dir (this object resource_dir or a group resource_dir or the default_resource_dir).
//So if resource_path is not defined, keys resource_name and a resource_dir should be defined.
'resource_path' => 'some_path',
//Optional key. Only the resource name (so without the directory).
'resource_name' => 'some_ressource.yml',
//Optional key. Override default_provider_method.
'provider_method' => 'some_method_name',
],
],
],
//Optional section.
'cache' =>
[
//Optional section. The cache for mapping metadata.
'metadata_cache' =>
[
//A cache instance which implements Kassko\Cache\CacheInterface. Default is Kassko\Cache\ArrayCache.
//If you use a third-party cache provider, maybe you need to wrap it into an adapter to enforce compatibility with Kassko\Cache\CacheInterface.
'instance' => $someCacheInstance,
//Default value is 0.
//0 means the data will never been deleted from the cache.
//Obviously, 'life_time' has no sense with an "ArrayCache" implementation.
'life_time' => 0,
//Default value is false. Indicates if the cache is shared or not.
//If you don't specify it, you're not wrong. It optimises the
'is_shared' => false,
],
//Optional section. The cache for query results.
//This section has the same keys as 'metadata_cache' section.
'result_cache': => [],
],
//Optional key. A logger instance which implements Psr\Logger\LoggerInterface.
'logger' => $logger,
//Optional key. Needed to retrieve repositories specified in 'repository_class' mapping attributes and which creation is assigned to a creator (a factory, a container, a callable).
'class_resolver' => $someClassResolver,
//Optional key. Needed to retrieve object listener specified in 'object_listener' mapping attributes and which creation is assigned to a creator (a factory, a container, a callable).
'object_listener_resolver' => $someObjectListenerResolver,
]
(1) availables types are annotations, yaml_file, php_file, inner_php, inner_yaml. And maybe others if you add some custom mapping loaders.
Annotation format:
use Kassko\DataMapper\Annotation as DM;
class SomeClass
{
/**
* @DM\Config(
* class="\ValueObjectClass",
* mappingResourceName="valueObjectResourceName",
* mappingResourcePath="valueObjectResourcePath",
* mappingResourceType="valueObjectResourceType"
* )
*/
protected $firstField;
Yaml format:
fields: [firstField]
config:
firstField:
class: "\\\ValueObjectClass"
mappingResourceName: valueObjectResourceName
mappingResourcePath: valueObjectResourcePath
mappingResourceType: valueObjectResourceType
Php format:
[
'fields' => ['firstField'],
'config' => [
'firstField' => [
'class' => '\ValueObjectClass',
'mappingResourceName' => 'valueObjectResourceName',
'mappingResourcePath' => 'valueObjectResourcePath',
'mappingResourceType' => 'valueObjectResourceType'
]
]
];
Annotation format:
use Kassko\DataMapper\Annotation as DM;
/**
* Class SomeClass
*
* @DM\CustomHydrator(
* class="SomeClassHydrator",
* hydrateMethod="hydrateMethod",
* extractMethod="extractMethod"
* )
*/
class SomeClass
{
}
Yaml format:
object:
customHydrator:
class: SomeClassHydrator
hydrateMethod: hydrateMethod
extractMethod: extractMethod
Php format:
[
'object' => [
'customHydrator' => [
'class' => 'SomeClassHydrator',
'hydrateMethod' => 'hydrateMethod',
'extractMethod' => 'extractMethod'
]
]
];
The methods prototype:
class SomeClassHydrator
{
/**
* Hydrate a SomeClass object from raw data $data.
*
* @param array The raw data
* @return SomeClass
*/
public function hydrateMethod(array $data)
{
}
/**
* Extract data from a SomeClass instance $object.
*
* @param SomeClass The object
* @return array
*/
public function extractMethod(SomeClass $object)
{
}
}
Annotation format:
use Kassko\DataMapper\Annotation as DM;
class SomeClass
{
/**
* @DM\DataSource(
* id="firstFieldId",
* lazyLoading=true,
* supplySeveralFields=true,
* depends={"depend#1","depend#2"},
* onFail="checkException",
* exceptionClass="\RuntimeException",
* badReturnValue="emptyString",
* fallbackSourceId="firstFieldFallbackSourceId",
* preprocessor=@DM\Method(method="fooPreprocessor"),
* processors = @DM\Methods({
* @DM\Method(method="barProcessor"),
* @DM\Method(method="bazProcessor")
* })
* class="\stdClass",
* method="someMethod",
* args={"argument#1", "argument#2"}
* )
*/
protected $firstField;
}
Yaml format:
fields:
firstField:
name: originalFieldName
dataSource:
id: firstFieldId
lazyLoading: true
supplySeveralFields: true
depends: [depend#1, depend#2]
onFail: checkException
exceptionClass: "\\\RuntimeException"
badReturnValue: emptyString
fallbackSourceId: firstFieldFallbackSourceId
preprocessor:
class: "##this"
method: fooPreprocessor
args: []
processor: ~
preprocessors: []
processors:
- {method: barProcessor}
- {method: bazProcessor}
class: "\\\stdClass"
method: someMethod
args: [argument#1, argument#2]
Php format:
[
'fields' => [
'firstField' => [
'name' => 'originalFieldName',
'dataSource' => [
'id' => 'firstFieldId',
'lazyLoading' => true,
'supplySeveralFields' => true,
'depends' => ['depend#1', 'depend#2'],
'onFail' => 'checkException',
'exceptionClass' => '\RuntimeException',
'badReturnValue' => 'emptyString',
'fallbackSourceId' => 'firstFieldFallbackSourceId',
'preprocessor' => [
'class' => '##this',
'method' => 'fooPreprocessor',
'args' => []
],
'processor' => [],
'preprocessors' => [],
'processors' => [
['method' => 'barProcessor'],
['method' => 'bazProcessor'],
],
'class' => '\stdClass',
'method' => 'someMethod',
'args' => ['argument#1', 'argument#2']
]
]
]
];
To know more about the "Method config" usage, please see its dedicated documentation "Method config".
Annotation format:
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\DataSourcesStore({
* @DM\DataSource(
* id="personSource",
* class="Kassko\Sample\PersonDataSource",
* method="getData",
* args="#id",
* lazyLoading=true,
* supplySeveralFields=true,
* onFail="checkException",
* exceptionClass="\RuntimeException",
* badReturnValue="emptyString",
* fallbackSourceId="testFallbackSourceId",
* depends="#dependsFirst",
* preprocessor = @DM\Method(method="somePreprocessor"),
* processor = @DM\Method(method="someProcessor")
* )
* })
*/
class SomeClass
{
}
Yaml format:
object:
dataSourcesStore:
- id: personSource
class: "Kassko\\\Sample\\\PersonDataSource"
method: getData
args: [#id]
lazyLoading: true
supplySeveralFields: true
onFail: checkException
exceptionClass: \RuntimeException
badReturnValue: emptyString
fallbackSourceId: testFallbackSourceId
depends: [#dependsFirst]
preprocessor:
class: ""
method: somePreprocessor
args: []
processor:
class: ""
method: someProcessor
args: []
Php format:
[
'object' => [
'dataSourcesStore' => [
[
'id'=> 'personSource',
'class'=> 'Kassko\Sample\PersonDataSource',
'method'=> 'getData',
'args' => ['#id'],
'lazyLoading' => true,
'supplySeveralFields' => true,
'onFail' => 'checkException',
'exceptionClass' => '\RuntimeException',
'badReturnValue' => 'emptyString',
'fallbackSourceId' => 'testFallbackSourceId',
'depends' => ['#dependsFirst'],
'preprocessor' => [
'class' => '',
'method' => 'somePreprocessor',
'args' => []
],
'processor' => [
'class' => '',
'method' => 'someProcessor',
'args' => []
]
]
]
]
];
To know more about the "Method config" usage, please see its dedicated documentation "Method config".
Annotation format:
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\RefImplicitSource(id="RefImplicitSourceId")
*/
class SomeClass
{
protected $fieldToBindAutoToImplicitSource;
protected $anotherFieldToBindAutoToImplicitSource;
/**
*@DM\ExcludeImplicitSource
*/
protected $fieldNotToBindAutoToImplicitSource;
}
Yaml format:
object:
RefImplicitSource: RefImplicitSourceId
fields:
fieldToBindAutoToImplicitSource:
name: fieldToBindAutoToImplicitSource
anotherFieldToBindAutoToImplicitSource:
name: anotherFieldToBindAutoToImplicitSource
fieldNotToBindAutoToImplicitSource:
name: fieldNotToBindAutoToImplicitSource
fieldsNotToBindToImplicitSource: [fieldNotToBindAutoToImplicitSource]
Php format:
[
'object' => [
'RefImplicitSource' => 'RefImplicitSourceId'
],
'fields' => [
'fieldToBindAutoToImplicitSource' => [
'name' => 'fieldToBindAutoToImplicitSource',
],
'anotherFieldToBindAutoToImplicitSource' => [
'name' => 'anotherFieldToBindAutoToImplicitSource',
],
'fieldNotToBindAutoToImplicitSource' => [
'name' => 'fieldNotToBindAutoToImplicitSource',
]
],
'fieldsNotToBindToImplicitSource' => [
'fieldNotToBindAutoToImplicitSource'
]
];
Annotation format:
use Kassko\DataMapper\Annotation as DM;
class SomeClass
{
/**
* @DM\Field(
* name="FirstField",
* type="string",
* class="stdClass",
* readConverter="readConvertFirstField",
* writeConverter="writeConvertFirstField",
* fieldMappingExtensionClass="ExtensionClass"
* )
*/
protected $fieldOne;
/**
* @DM\Field(
* name="SecondField",
* type="integer",
* class="\DateTime",
* readDateConverter="readDateConvertSecondField",
* writeDateConverter="writeDateConvertSecondField",
* fieldMappingExtensionClass="ExtensionClass",
* defaultValue="12"
* )
*/
protected $fieldTwo;
/**
* @DM\Field(
* name="DateField",
* type="date"
* )
*/
protected $dateField;
}
Yaml format:
fields:
fieldOne:
name: FirstField
type: string
class: stdClass
readConverter: readConvertFirstField
writeConverter: writeConvertFirstField
fieldMappingExtensionClass: ExtensionClass
fieldTwo:
name: SecondField
type: integer
class: "\\\DateTime"
readDateConverter: readDateConvertSecondField
writeDateConverter: writeDateConvertSecondField
fieldMappingExtensionClass: ExtensionClass
defaultValue: 12
dateField:
name: DateField
type: date
Php format:
[
'fields' => [
'fieldOne' => [
'name' => 'FirstField',
'type' => 'string',
'class' => 'stdClass',
'readConverter' => 'readConvertFirstField',
'writeConverter' => 'writeConvertFirstField',
'fieldMappingExtensionClass' => 'ExtensionClass',
],
'fieldTwo' => [
'name' => 'SecondField',
'type' => 'integer',
'class' => '\DateTime',
'readDateConverter' => 'readDateConvertSecondField',
'writeDateConverter' => 'writeDateConvertSecondField',
'fieldMappingExtensionClass' => 'ExtensionClass',
'defaultValue' => 12
],
'dateField' => [
'name' => 'DateField',
'type' => 'date'
]
]
];
Annotation format:
use Kassko\DataMapper\Annotation as DM;
class SomeClass
{
/**
* @DM\Getter(
* name="getterName"
* )
*/
protected $firstField;
/**
* @DM\Getter(
* prefix="is"
* )
*/
protected $secondField;
}
Yaml format:
fields:
firstField:
name: firstField
getter:
name: getterName
secondField:
name: secondField
getter:
prefix: is
Php format:
[
'fields' => [
'firstField' => [
'name' => 'firstField',
'getter' => ['name' => 'getterName'],
],
'secondField' => [
'name' => 'secondField',
'getter' => ['prefix' => 'is'],
],
]
];
Annotation format:
use Kassko\DataMapper\Annotation as DM;
class SomeClass
{
/**
* @DM\Id
*/
protected $firstField;
}
Yaml format:
id: firstField
fields: ["firstField"]
Php format:
[
'id' => 'firstField',
'fields' => ['firstField']
];
Annotation format:
use Kassko\DataMapper\Annotation as DM;
class SomeClass
{
/**
* @DM\IdCompositePart
*/
protected $firstField;
/**
* @DM\IdCompositePart
*/
protected $secondField;
}
Yaml format:
idComposite: [firstField, secondField]
fields: [firstField, secondField]
Php format:
[
'idComposite' => ['firstField', 'secondField'],
'fields' => ['firstField', 'secondField']
];
Annotation format:
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\Listeners(
* preHydrate = @DM\Methods({
* @DM\Method(class="SomeClass", method="preHydrateMethodName")
* }),
* postHydrate = @DM\Methods({
* @DM\Method(class="SomeClass", method="postHydrateMethodName"),
* @DM\Method(class="SomeClassB", method="postHydrateMethodName")
* }),
* preExtract = @DM\Methods({
* @DM\Method(class="SomeClass", method="preExtractMethodName", args="foo")
* }),
* postExtract = @DM\Methods({
* @DM\Method(class="SomeClass", method="postExtractMethodName", args={"foo", "#bar"})
* })
* )
*/
class SomeClass
{
}
Yaml format:
listeners:
preHydrate:
- {class: SomeClass, method: preHydrateMethodName}
postHydrate:
- {class: SomeClass, method: postHydrateMethodName}
- {class: SomeClassB, method: postHydrateMethodName}
preExtract:
- {class: SomeClass, method: preExtractMethodName, args: foo}
postExtract:
- {class: SomeClass, method: postExtractMethodName, args: ['foo', '#bar']}
Php format:
[
'listeners' => [
'preHydrate' => ['class' => 'SomeClass', 'method' => 'preHydrateMethodName'],
'postHydrate' =>
[
['class' => 'SomeClass', 'method' => 'postHydrateMethodName'],
['class' => 'SomeClassB', 'method' => 'postHydrateMethodName'],
],
'preExtract' => ['class' => 'SomeClass', 'method' => 'preExtractMethodName', 'args' => 'foo'],
'postExtract' => ['class' => 'SomeClass', 'method' => 'postExtractMethodName', 'args' => ['foo', '#bar']],
]
];
Annotation format:
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\Method(method="someMethod")
* @DM\Method(class="SomeClass", method="someMethod", args="abc")
* @DM\Method(class="@some_object_created_from_factory_or_container", method="someMethod", args= {"##this", "abc"}
*/
Yaml format:
some_key:
'method': someMethod
some_key:
class: SomeClass
method: someMethod
args: abc
some_key:
class: @some_object_created_from_factory_or_container
method: someMethod
args: ['##this', 'abc']
Php format:
[
'method' => 'someMethod',
]
[
'class' => 'SomeClass',
'method' => 'someMethod',
'args' => 'abc'
]
[
'class' => '@some_object_created_from_factory_or_container',
'method' => 'someMethod',
'args' => ['##this', 'abc']
]
Annotation format:
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\Object(
* fieldExclusionPolicy="exclude_all",
* providerClass="testProviderClass",
* readDateConverter="testReadDateConverter",
* writeDateConverter="testWriteDateConverter",
* propertyAccessStrategy=true,
* fieldMappingExtensionClass="testFieldMappingExtensionClass",
* classMappingExtensionClass="testClassMappingExtensionClass"
* )
*/
class SomeClass
{
}
Yaml format:
fieldExclusionPolicy: exclude_all
object:
providerClass: testProviderClass
readDateConverter: testReadDateConverter
writeDateConverter: testWriteDateConverter
propertyAccessStrategy: true
fieldMappingExtensionClass: testFieldMappingExtensionClass
classMappingExtensionClass: testClassMappingExtensionClass
Php format:
[
'fieldExclusionPolicy' => 'exclude_all',
'object' => [
'providerClass' => 'testProviderClass',
'readDateConverter' => 'testReadDateConverter',
'writeDateConverter' => 'testWriteDateConverter',
'propertyAccessStrategy'=> true,
'fieldMappingExtensionClass' => 'testFieldMappingExtensionClass',
'classMappingExtensionClass' => 'testClassMappingExtensionClass'
]
];
Annotation format:
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\ObjectListeners(
* classList={"SomeListenerAClass", "SomeListenerBClass"}
* )
*/
class SomeClass
{
}
Yaml format:
objectListeners: [SomeListenerAClass, SomeListenerBClass]
Php format:
[
'objectListeners' => ['SomeListenerAClass', 'SomeListenerBClass']
];
Deprecated. Use Listeners config instead.
Annotation format:
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\Object(classMappingExtensionClass="mappingExtensionClass")
*
* @DM\PostExtract(method="postExtractMethodName")
*/
class SomeClass
{
}
Yaml format:
object:
classMappingExtensionClass: mappingExtensionClass
interceptors:
postExtract: postExtractMethodName
Php format:
[
'object' => ['classMappingExtensionClass' => 'mappingExtensionClass'],
'interceptors' => [
'postExtract' => 'postExtractMethodName'
]
];
Deprecated. Use Listeners config instead.
Annotation format:
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\Object(classMappingExtensionClass="mappingExtensionClass")
*
* @DM\PostHydrate(method="postHydrateMethodName")
*/
class SomeClass
{
}
Yaml format:
object:
classMappingExtensionClass: mappingExtensionClass
interceptors:
postHydrate: postHydrateMethodName
Php format:
[
'object' => ['classMappingExtensionClass' => 'mappingExtensionClass'],
'interceptors' => [
'postHydrate' => 'postHydrateMethodName'
]
];
Deprecated. Use Listeners config instead.
Annotation format:
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\Object(classMappingExtensionClass="mappingExtensionClass")
*
* @DM\PreExtract(
* method="preExtractMethodName"
* )
*/
class SomeClass
{
}
Yaml format:
object:
classMappingExtensionClass: mappingExtensionClass
interceptors:
preExtract: preExtractMethodName
Php format:
[
'object' => ['classMappingExtensionClass' => 'mappingExtensionClass'],
'interceptors' => [
'preExtract' => 'preExtractMethodName'
]
];
Deprecated. Use Listeners config instead.
Annotation format:
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\Object(classMappingExtensionClass="mappingExtensionClass")
*
* @DM\PreHydrate(method="preHydrateMethodName")
*/
class SomeClass
{
}
Yaml format:
object:
classMappingExtensionClass: mappingExtensionClass
interceptors:
preHydrate: preHydrateMethodName
Php format:
[
'object' => ['classMappingExtensionClass' => 'mappingExtensionClass'],
'interceptors' => [
'preHydrate' => 'preHydrateMethodName'
]
];
Deprecated. Use DataSource config instead. Configuration is the same as DataSource.
Deprecated. Use DataSourcesStore config instead. Configuration is the same as DataSourceStore.
Annotation format:
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\RefImplicitSource(id="RefImplicitSourceId")
*/
class SomeClass
{
}
Yaml format:
object:
RefImplicitSource: RefImplicitSourceId
fields:
mockField:
name: mockFieldName
Php format:
[
'object' => [
'RefImplicitSource' => 'RefImplicitSourceId'
],
'fields' => [
'mockField' => [
'name' => 'mockFieldName',
]
]
];
Annotation format:
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\DataSourcesStore({
* @DM\DataSource(
* id="someDataSource",
* class="Kassko\Sample\PersonDataSource",
* method="getData"
* )
* })
*/
class SomeClass
{
/**
* @DM\RefSource(id="someDataSource")
*/
private $fieldOne;
}
Yaml format:
object:
dataSourcesStore:
- id: personSource
class: "Kassko\\\Sample\\\PersonDataSource"
method: getData
fields:
fieldOne:
name: fieldOne
refSource: someDataSource
Php format:
[
'object' => [
'dataSourcesStore' => [
[
'id'=> 'personSource',
'class'=> 'Kassko\Sample\PersonDataSource',
'method'=> 'getData',
]
]
],
'fields' => [
'fieldOne' => [
'name' => 'fieldOne',
'refSource' => 'someDataSource',
]
],
];
### Setter config
Annotation format:
```php
use Kassko\DataMapper\Annotation as DM;
class SomeClass
{
/**
* @DM\Setter(
* prefix="setterPrefix",
* name="setterName"
* )
*/
protected $firstField;
}
Yaml format:
fields:
firstField:
name: firstField
setter:
name: setterName
Php format:
[
'fields' => [
'firstField' => [
'name' => 'firstField',
'setter' => ['name' => 'setterName'],
]
]
];
Annotation format:
use Kassko\DataMapper\Annotation as DM;
/**
* Optional because it's the default settings for fieldExclusionPolicy.
* @DM\Object(fieldExclusionPolicy="include_all")
*/
class SomeClass
{
/**
* @DM\ToExclude
*/
protected $excludedField;
/**
* @DM\Field
*/
protected $anotherField;
}
Yaml format:
# Optional because it's the default settings for fieldExclusionPolicy.
# object:
# fieldExclusionPolicy: include_all
excludedFields: [excludedField]
fields:
excludedField:
name: excludedField
anotherField:
name: anotherField
Php format:
[
/*
//Optional because it's the default settings for fieldExclusionPolicy.
'object' => [
'fieldExclusionPolicy' => 'include_all'
],
*/
'excludedFields' => [
'excludedField'
],
'fields' => [
'excludedField' => [
'name' => 'excludedField'
],
'anotherField' => [
'name' => 'anotherField'
]
]
];
Annotation format:
use Kassko\DataMapper\Annotation as DM;
/**
* @DM\Object(fieldExclusionPolicy="exclude_all")
*/
class SomeClass
{
/**
* @DM\Include
*/
protected $includedField;
/**
* @DM\Field
*/
protected $field;
}
Yaml format:
fieldExclusionPolicy: exclude_all
fieldsToInclude: [includedField]
fields:
includedField:
name: includedField
anotherField:
name: anotherField
Php format:
[
'fieldExclusionPolicy' => 'exclude_all'
'fieldsToInclude' => [
'includedField'
],
'fields' => [
'includedField' => [
'name' => 'includedField'
],
'anotherField' => [
'name' => 'anotherField'
],
]
];
Annotation format:
use Kassko\DataMapper\Annotation as DM;
class SomeClass
{
/**
* @DM\Transient
*/
protected $firstField;
/**
* @var string
*/
protected $secondField;
}
Yaml format:
transient: [firstField]
fields: [firstField]
Php format:
[
'transient' => ['firstField'],
'fields' => [
'firstField' => [
'name' => 'firstFieldName'
]
]
];
Deprecated. Use Config config instead.
Annotation format: Annotation @Kassko\DataMapper\Annotation\ValueObject
becomes @Kassko\DataMapper\Annotation\Config
.
Yaml and Php format: Now, there's a field key 'fields.some_field.config'
. Uses it instead of the global key 'valueObjects'
.
Annotation format:
use Kassko\DataMapper\Annotation as DM;
class ValueObject
{
/**
* @DM\Config(
* class="\ValueObjectClass",
* mappingResourceName="valueObjectResourceName",
* mappingResourcePath="valueObjectResourcePath",
* mappingResourceType="valueObjectResourceType"
* )
*/
protected $firstField;
}
Yaml format:
fields:
firstField:
name: firstField
valueObjects:
firstField:
class: "\\\ValueObjectClass"
mappingResourceName: valueObjectResourceName
mappingResourcePath: valueObjectResourcePath
mappingResourceType: valueObjectResourceType
Php format:
[
'fields' => [
'firstField' => [
'name' => 'firstField',
]
],
'valueObjects' => [
'firstField' => [
'class' => '\ValueObjectClass',
'mappingResourceName' => 'valueObjectResourceName',
'mappingResourcePath' => 'valueObjectResourcePath',
'mappingResourceType' => 'valueObjectResourceType'
]
]
];
Annotation format:
use Kassko\DataMapper\Annotation as DM;
class SomeClass
{
/**
* @DM\Version
*/
protected $firstField;
}
Yaml format:
version: firstField
fields: [firstField]
Php format:
[
'version' => 'firstField',
'fields' => ['firstField']
];
========================================
Result builder in details
Map some property names to keys of your raw datas
Convert some values before hydration or before extraction
Use a data source for a property
Lazy load some properties
Use a data source for a set of properties
Hydrate nested object or nested collections
Use fallback data sources
Use local config
Select the fields to map
Getters, isser, hasser and more get methods
Choose or change an object mapping configurations at runtime
Choose your mapping configuration format
Configuration reference documentation
Inner Yaml mapping reference documentation
.Yaml mapping reference documentation
.Inner php mapping reference documentation
.Php mapping reference documentation
.Author: kassko
Source Code: https://github.com/kassko/data-mapper
License: MIT license
#php
1667279100
Jekyll
plugin for Astronauts.
Spaceship is a minimalistic, powerful and extremely customizable Jekyll plugin. It combines everything you may need for convenient work, without unnecessary complications, like a real spaceship.
💡 Tip: I hope you enjoy using this plugin. If you like this project, a little star for it is your way make a clear statement: My work is valued. I would appreciate your support! Thank you!
Add jekyll-spaceship plugin in your site's Gemfile
, and run bundle install
.
# If you have any plugins, put them here!
group :jekyll_plugins do
gem 'jekyll-spaceship'
end
Or you better like to write in one line:
gem 'jekyll-spaceship', group: :jekyll_plugins
Add jekyll-spaceship to the plugins:
section in your site's _config.yml
.
plugins:
- jekyll-spaceship
💡 Tip: Note that GitHub Pages runs in safe
mode and only allows a set of whitelisted plugins. To use the gem in GitHub Pages, you need to build locally or use CI (e.g. travis, github workflow) and deploy to your gh-pages
branch.
This plugin runs with the following configuration options by default. Alternative settings for these options can be explicitly specified in the configuration file _config.yml
.
# Where things are
jekyll-spaceship:
# default enabled processors
processors:
- table-processor
- mathjax-processor
- plantuml-processor
- mermaid-processor
- polyfill-processor
- media-processor
- emoji-processor
- element-processor
mathjax-processor:
src:
- https://polyfill.io/v3/polyfill.min.js?features=es6
- https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
config:
tex:
inlineMath:
- ['$','$']
- ['\(','\)']
displayMath:
- ['$$','$$']
- ['\[','\]']
svg:
fontCache: 'global'
optimize: # optimization on building stage to check and add mathjax scripts
enabled: true # value `false` for adding to all pages
include: [] # include patterns for math expressions checking (regexp)
exclude: [] # exclude patterns for math expressions checking (regexp)
plantuml-processor:
mode: default # mode value 'pre-fetch' for fetching image at building stage
css:
class: plantuml
syntax:
code: 'plantuml!'
custom: ['@startuml', '@enduml']
src: http://www.plantuml.com/plantuml/svg/
mermaid-processor:
mode: default # mode value 'pre-fetch' for fetching image at building stage
css:
class: mermaid
syntax:
code: 'mermaid!'
custom: ['@startmermaid', '@endmermaid']
config:
theme: default
src: https://mermaid.ink/svg/
media-processor:
default:
id: 'media-{id}'
class: 'media'
width: '100%'
height: 350
frameborder: 0
style: 'max-width: 600px; outline: none;'
allow: 'encrypted-media; picture-in-picture'
emoji-processor:
css:
class: emoji
src: https://github.githubassets.com/images/icons/emoji/
For now, these extended features are provided:
Noted that GitHub filters out style property, so the example displays with the obsolete align property. But in actual this plugin outputs style property with text-align CSS attribute.
^^ in a cell indicates it should be merged with the cell above.
This feature is contributed by pmccloghrylaing.
| Stage | Direct Products | ATP Yields |
| -----------------: | --------------: | ---------: |
| Glycolysis | 2 ATP ||
| ^^ | 2 NADH | 3--5 ATP |
| Pyruvaye oxidation | 2 NADH | 5 ATP |
| Citric acid cycle | 2 ATP ||
| ^^ | 6 NADH | 15 ATP |
| ^^ | 2 FADH | 3 ATP |
| 30--32 ATP |||
Code above would be parsed as:
Stage | Direct Products | ATP Yields |
---|---|---|
Glycolysis | 2 ATP | |
2 NADH | 3–5 ATP | |
Pyruvaye oxidation | 2 NADH | 5 ATP |
Citric acid cycle | 2 ATP | |
6 NADH | 15 ATP | |
2 FADH2 | 3 ATP | |
30–32 ATP |
A backslash at end to join cell contents with the following lines.
This feature is contributed by Lucas-C.
| : Easy Multiline : |||
| :----- | :----- | :------ |
| Apple | Banana | Orange \
| Apple | Banana | Orange \
| Apple | Banana | Orange
| Apple | Banana | Orange \
| Apple | Banana | Orange |
| Apple | Banana | Orange |
Code above would be parsed as:
Easy Multiline | ||
---|---|---|
Apple Apple Apple | Banana Banana Banana | Orange Orange Orange |
Apple Apple | Banana Banana | Orange Orange |
Apple | Banana | Orange |
Table header can be eliminated.
|--|--|--|--|--|--|--|--|
|♜| |♝|♛|♚|♝|♞|♜|
| |♟|♟|♟| |♟|♟|♟|
|♟| |♞| | | | | |
| |♗| | |♟| | | |
| | | | |♙| | | |
| | | | | |♘| | |
|♙|♙|♙|♙| |♙|♙|♙|
|♖|♘|♗|♕|♔| | |♖|
Code above would be parsed as:
♜ | ♝ | ♛ | ♚ | ♝ | ♞ | ♜ | |
♟ | ♟ | ♟ | ♟ | ♟ | ♟ | ||
♟ | ♞ | ||||||
♗ | ♟ | ||||||
♙ | |||||||
♘ | |||||||
♙ | ♙ | ♙ | ♙ | ♙ | ♙ | ♙ | |
♖ | ♘ | ♗ | ♕ | ♔ | ♖ |
Markdown table syntax use colons ":" for forcing column alignment.
Therefore, here we also use it for forcing cell alignment.
Table cell can be set alignment separately.
| : Fruits \|\| Food : |||
| :--------- | :-------- | :-------- |
| Apple | : Apple : | Apple \
| Banana | Banana | Banana \
| Orange | Orange | Orange |
| : Rowspan is 4 : || How's it? |
|^^ A. Peach || 1. Fine :|
|^^ B. Orange ||^^ 2. Bad |
|^^ C. Banana || It's OK! |
Code above would be parsed as:
Fruits || Food | ||
---|---|---|
Apple Banana Orange | Apple Banana Orange | Apple Banana Orange |
Rowspan is 4 A. Peach B. Orange C. Banana | ||
How's it? | ||
1. Fine 2. Bad | ||
It' OK! |
Sometimes we may need some abundant content (e.g., mathjax, image, video) in Markdown table
Therefore, here we also make markown syntax possible inside a cell.
| : MathJax \|\| Image : |||
| :------------ | :-------- | :----------------------------- |
| Apple | : Apple : | Apple \
| Banana | Banana | Banana \
| Orange | Orange | Orange |
| : Rowspan is 4 : || : How's it? : |
| ^^ A. Peach || 1. ![example][cell-image] |
| ^^ B. Orange || ^^ 2. $I = \int \rho R^{2} dV$ |
| ^^ C. Banana || **It's OK!** |
[cell-image]: https://jekyllrb.com/img/octojekyll.png "An exemplary image"
Code above would be parsed as:
MathJax || Image | ||
---|---|---|
Apple Banana Orange | Apple Banana Orange | Apple Banana Orange |
Rowspan is 4 A. Peach B. Orange C. Banana | ||
How's it? | ||
It' OK! |
This feature is very useful for custom cell such as using inline style. (e.g., background, color, font)
The idea and syntax comes from the Maruku package.
Following are some examples of attributes definitions (ALDs) and afterwards comes the syntax explanation:
{:ref-name: #id .cls1 .cls2}
{:second: ref-name #id-of-other title="hallo you"}
{:other: ref-name second}
An ALD line has the following structure:
If there is more than one ALD with the same reference name, the attribute definitions of all the ALDs are processed like they are defined in one ALD.
An inline attribute list (IAL) is used to attach attributes to another element.
Here are some examples for span IALs:
{: #id .cls1 .cls2} <!-- #id <=> id="id", .cls1 .cls2 <=> class="cls1 cls2" -->
{: ref-name title="hallo you"}
{: ref-name class='.cls3' .cls4}
Here is an example for custom table cell with IAL:
{:color-style: style="background: black;"}
{:color-style: style="color: white;"}
{:text-style: style="font-weight: 800; text-decoration: underline;"}
|: Here's an Inline Attribute Lists example :||||
| ------- | ------------------ | -------------------- | ------------------ |
|: :|: <div style="color: red;"> < Normal HTML Block > </div> :|||
| ^^ | Red {: .cls style="background: orange" } |||
| ^^ IALs | Green {: #id style="background: green; color: white" } |||
| ^^ | Blue {: style="background: blue; color: white" } |||
| ^^ | Black {: color-style text-style } |||
Code above would be parsed as:
Additionally, here you can learn more details about IALs.
MathJax is an open-source JavaScript display engine for LaTeX, MathML, and AsciiMath notation that works in all modern browsers.
Some of the main features of MathJax include:
At building stage, the MathJax engine script will be added by automatically checking whether there is a math expression in the page, this feature can help you improve the page performance on loading speed.
Put your math expression within $...$
$ a * b = c ^ b $
$ 2^{\frac{n-1}{3}} $
$ \int\_a^b f(x)\,dx. $
Code above would be parsed as:
PlantUML is a component that allows to quickly write:
There are two ways to create a diagram in your Jekyll blog page:
```plantuml!
Bob -> Alice : hello world
```
or
@startuml
Bob -> Alice : hello
@enduml
Code above would be parsed as:
Mermaid is a Javascript based diagramming and charting tool. It generates diagrams flowcharts and more, using markdown-inspired text for ease and speed.
It allows to quickly write:
There are two ways to create a diagram in your Jekyll blog page:
```mermaid!
pie title Pets adopted by volunteers
"Dogs" : 386
"Cats" : 85
"Rats" : 35
```
or
@startmermaid
pie title Pets adopted by volunteers
"Dogs" : 386
"Cats" : 85
"Rats" : 35
@endmermaid
Code above would be parsed as:
How often did you find yourself googling "How to embed a video/audio in markdown?"
While its not possible to embed a video/audio in markdown, the best and easiest way is to extract a frame from the video/audio. To add videos/audios to your markdown files easier I developped this tool for you, and it will parse the video/audio link inside the image block automatically.
For now, these media links parsing are provided:
There are two ways to embed a video/audio in your Jekyll blog page:
Inline-style:

Reference-style:
![][{reference}]
[{reference}]: {media-link}
For configuring media attributes (e.g, width, height), just adding query string to the link as below:
















As markdown is not only a lightweight markup language with plain-text-formatting syntax, but also an easy-to-read and easy-to-write plain text format, so writing a hybrid HTML with markdown is an awesome choice.
It's easy to write markdown inside HTML:
<script type="text/markdown">
# Hybrid HTML with Markdown is a not bad choice ^\_^
## Table Usage
| : Fruits \|\| Food : |||
| :--------- | :-------- | :-------- |
| Apple | : Apple : | Apple \
| Banana | Banana | Banana \
| Orange | Orange | Orange |
| : Rowspan is 4 : || How's it? |
|^^ A. Peach || 1. Fine :|
|^^ B. Orange ||^^ 2. Bad |
|^^ C. Banana || It's OK! |
## PlantUML Usage
@startuml
Bob -> Alice : hello
@enduml
## Video Usage

</script>
It allows us to polyfill features for extending markdown syntax.
For now, these polyfill features are provided:
A backslash at begin to escape the ordered list.
Normal:
1. List item Apple.
3. List item Banana.
10. List item Cafe.
Escaped:
\1. List item Apple.
\3. List item Banana.
\10. List item Cafe.
Code above would be parsed as:
Normal:
1. List item Apple.
2. List item Banana.
3. List item Cafe.
Escaped:
1. List item Apple.
3. List item Banana.
10. List item Cafe.
GitHub-flavored emoji images and names would allow emojifying content such as: it's raining :cat:s and :dog:s!
Noted that emoji images are served from the GitHub.com CDN, with a base URL of https://github.githubassets.com, which results in emoji image URLs like https://github.githubassets.com/images/icons/emoji/unicode/1f604.png.
In any page or post, use emoji as you would normally, e.g.
I give this plugin two :+1:!
Code above would be parsed as:
I give this plugin two :+1:!
If you'd like to serve emoji images locally, or use a custom emoji source, you can specify so in your _config.yml
file:
jekyll-spaceship:
emoji-processor:
src: "/assets/images/emoji"
See the Gemoji documentation for generating image files.
It allows us to modify elements via CSS3 selectors
. Through it you can easily modify the attributes of an element tag, replace the children nodes and so on, it's very flexible, but here is example usage for modifying a document:
# Here is a comprehensive example
jekyll-spaceship:
element-processor:
css:
- a: '<h1>Test</h1>' # Replace all `a` tags (String Style)
- ['a.link1', 'a.link2']: # Replace all `a.link1`, `a.link2` tags (Hash Style)
name: img # Replace element tag name
props: # Replace element properties
title: Good image # Add a title attribute
src: ['(^.*$)', '\0?a=123'] # Add query string to src attribute by regex pattern
style: # Add style attribute (Hash Style)
color: red
font-size: '1.2em'
children: # Add children to the element
- # First empty for adding after the last child node
- "<span>Google</span>" # First child node (String Style)
- # Middle empty for wrapping the children nodes
- name: span # Second child node (Hash Style)
props:
prop1: "1" # Custom property1
prop2: "2" # Custom property2
prop3: "3" # Custom property3
children: # Add nested chidren nodes
- "<span>Jekyll</span>" # First child node (String Style)
- name: span # Second child node (Hash Style)
props: # Add attributes to child node (Hash Style)
prop1: "a"
prop2: "b"
prop3: "c"
children: "<b>Yap!</b>" # Add children nodes (String Style)
- # Last empty for adding before the first child node
- a.link: '<a href="//t.com">Link</a>' # Replace all `a.link` tags (String Style)
- 'h1#title': # Replace `h1#title` tags (Hash Style)
children: I'm a title! # Replace inner html to new text
Automatically adds a target="_blank" rel="noopener noreferrer"
attribute to all external links in Jekyll's content.
jekyll-spaceship:
element-processor:
css:
- a: # Replace all `a` tags
props:
class: ['(^.*$)', '\0 ext-link'] # Add `ext-link` to class by regex pattern
target: _blank # Replace `target` value to `_blank`
rel: noopener noreferrer # Replace `rel` value to `noopener noreferrer`
Automatically adds loading="lazy"
to img
and iframe
tags to natively load lazily. Browser support is growing. If a browser does not support the loading
attribute, it will load the resource just like it would normally.
jekyll-spaceship:
element-processor:
css:
- a: # Replace all `a` tags
props: #
loading: lazy # Replace `loading` value to `lazy`
In case you want to prevent loading some images/iframes lazily, add loading="eager"
to their tags. This might be useful to prevent flickering of images during navigation (e.g. the site's logo).
See the following examples to prevent lazy loading.
jekyll-spaceship:
element-processor:
css:
- a: # Replace all `a` tags
props: #
loading: eager # Replace `loading` value to `eager`
There are three options when using this method to lazy load images. Here are the supported values for the loading attribute:
Issues and Pull Requests are greatly appreciated. If you've never contributed to an open source project before I'm more than happy to walk you through how to create a pull request.
You can start by opening an issue describing the problem that you're looking to resolve and we'll go from there.
Author: jeffreytse
Source Code: https://github.com/jeffreytse/jekyll-spaceship
License: MIT license
1665851640
Drift
Proudly Sponsored by Stream 💙
Drift is a reactive persistence library for Flutter and Dart, built on top of sqlite. Drift is
WITH
and WINDOW
clauses.import
s in sql files, drift helps you keep your database code simple.With drift, persistence on Flutter is fun!
To start using drift, read our detailed docs.
If you have any questions, feedback or ideas, feel free to create an issue. If you enjoy this project, I'd appreciate your 🌟 on GitHub.
For information to use this library on the web (including Flutter web), follow the instructions here. Keep in mind that web support is still experimental.
Run this command:
With Dart:
$ dart pub add drift
With Flutter:
$ flutter pub add drift
This will add a line like this to your package's pubspec.yaml (and run an implicit dart pub get
):
dependencies:
drift: ^2.2.0
Alternatively, your editor might support dart pub get
or flutter pub get
. Check the docs for your editor to learn more.
Now in your Dart code, you can use:
import 'package:drift/drift.dart';
// For more information on using drift, please see https://drift.simonbinder.eu/docs/getting-started/
// A full cross-platform example is available here: https://github.com/simolus3/drift/tree/develop/examples/app
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
part 'main.g.dart';
@DataClassName('TodoCategory')
class TodoCategories extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
}
class TodoItems extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text()();
TextColumn get content => text().nullable()();
IntColumn get categoryId => integer().references(TodoCategories, #id)();
TextColumn get generatedText => text().nullable().generatedAs(
title + const Constant(' (') + content + const Constant(')'))();
}
abstract class TodoCategoryItemCount extends View {
TodoItems get todoItems;
TodoCategories get todoCategories;
Expression<int> get itemCount => todoItems.id.count();
@override
Query as() => select([
todoCategories.name,
itemCount,
]).from(todoCategories).join([
innerJoin(todoItems, todoItems.categoryId.equalsExp(todoCategories.id))
]);
}
@DriftView(name: 'customViewName')
abstract class TodoItemWithCategoryNameView extends View {
TodoItems get todoItems;
TodoCategories get todoCategories;
Expression<String> get title =>
todoItems.title +
const Constant('(') +
todoCategories.name +
const Constant(')');
@override
Query as() => select([todoItems.id, title]).from(todoItems).join([
innerJoin(
todoCategories, todoCategories.id.equalsExp(todoItems.categoryId))
]);
}
@DriftDatabase(tables: [
TodoItems,
TodoCategories,
], views: [
TodoCategoryItemCount,
TodoItemWithCategoryNameView,
])
class Database extends _$Database {
Database(QueryExecutor e) : super(e);
@override
int get schemaVersion => 2;
@override
MigrationStrategy get migration {
return MigrationStrategy(
onCreate: (m) async {
await m.createAll();
// Add a bunch of default items in a batch
await batch((b) {
b.insertAll(todoItems, [
TodoItemsCompanion.insert(title: 'A first entry', categoryId: 0),
TodoItemsCompanion.insert(
title: 'Todo: Checkout drift',
content: const Value('Drift is a persistence library for Dart '
'and Flutter applications.'),
categoryId: 0,
),
]);
});
},
);
}
// The TodoItem class has been generated by drift, based on the TodoItems
// table description.
//
// In drift, queries can be watched by using .watch() in the end.
// For more information on queries, see https://drift.simonbinder.eu/docs/getting-started/writing_queries/
Stream<List<TodoItem>> get allItems => select(todoItems).watch();
}
Future<void> main() async {
// Create an in-memory instance of the database with todo items.
final db = Database(NativeDatabase.memory());
db.allItems.listen((event) {
print('Todo-item in database: $event');
});
// Add category
final categoryId = await db
.into(db.todoCategories)
.insert(TodoCategoriesCompanion.insert(name: 'Category'));
// Add another entry
await db.into(db.todoItems).insert(TodoItemsCompanion.insert(
title: 'Another entry added later', categoryId: categoryId));
(await db.select(db.customViewName).get()).forEach(print);
(await db.select(db.todoCategoryItemCount).get()).forEach(print);
// Delete all todo items
await db.delete(db.todoItems).go();
}
Download Details:
Author: simolus3
Source Code: https://github.com/simolus3/drift
1665127784
This ongoing blog post is about understanding ways of extracting data from any website, either if the website is static/fully JavaScript driven or if reverse engineering needs to be applied.
After this blog post, you can understand and apply this knowledge to create structured data from messy data from any website.
At SerpApi we're extracting data from all sorts of websites and each one of them requires a specific approach that will able us to parse data fast and efficiently and most importantly without browser automation. We want to share a little of our knowledge gained while building our APIs.
📌 Some of the techniques will not be mentioned intentionally in order not to disclose some of the methods we use.
Shown tips should be plenty enough to get you going or understand where to potentially look to solve the problem.
🐍 This blog post uses Python as language to show code examples.
Dev tools is possibly one of the very first thing that needs to be checked before writing actual code.
I, wrote quite a lot of code before looking at dev tools with the realization that instead of parsing data, I can make a simple HTTP request, either GET
or POST
to the website API/server and get a JSON response with all the necessary data that being already structured.
Under dev tools, you can find a Elements, Network, and Source/Application tabs.
The follow-up sections will go from easiest to hardest methods with tools that can be used to make the job done.
CSS selectors are patterns used to select match the element(s) you want to style extract from HTML page.
While for building websites CSS selectors help to style the website, in webscraping those selectors used to match certain HTML nodes (elements) from which we want data to be extracted.
We've covered types of selectors with actual code examples in the web scraping with CSS selectors blog post and which selector works best for a particular task, and how to test them in the browser.
There're a few often-used type of selectors:
SELECTOR TYPE | SYNTAX | PURPOSE |
---|---|---|
Type Selectors | element_name | Matches HTML node based on element |
Class Selectors | .class_name | Matches HTML node based on class |
ID Selectors | #id_value | Matches HTML node based on ID |
Attribute Selectors | [attribute=attribute_value] /[attribute] | Extracts attribute data from HTML node |
Selector List | element, element, element, ... /selector, selector, selector, ... | Matches all nodes based on given selector(s). Great for handling different HTML layouts |
Descendant combinator | selector1 selector2 | Matches data from a second selector if they have an ancestor |
XPath is a query language that is used to navigate DOM and could be is more flexible than CSS selectors.
It's mostly helpful when HTML element doesn't have a selector name, or has a very weird location in a messy HTML. XPath uses expressions to create a query:
//div[@class='content']//text()[not(ancestor::div/@class[contains(., 'code')])][normalize-space()]
SelectorGadget extension allows to quickly grab CSS selector(s) by clicking on the desired element in your browser. It returns a CSS selector(s).
Example of selecting certain HTML elements and returned CSS selector(s):
We've shown how SelectorGadget works in a separate web scraping with CSS selectors blog post.
Selectors are not the only place where valuable data could be extracted. Some of the data can be directly in the HTML <meta>
elements. <meta>
element loads no matter if the page is static or dynamic.
For example, on Medium you can extract basic profile info from <meta>
elements:
import requestsfrom bs4 import BeautifulSouphtml = requests.get('https://medium.com/@dimitryzub').textsoup = BeautifulSoup(html, 'html.parser')description = soup.select('meta')[13]['content']link = soup.select('meta')[16]['content']profile_picture = soup.select('meta')[21]['content']print(description, link, profile_picture, sep='\n')'''Read writing from Dmitriy Zub ☀️ on Medium. Developer Advocate at SerpApi. I help to make structured data from a pile of mess.https://dimitryzub.medium.comhttps://miro.medium.com/max/2400/1*he-C8WKaIjHIe58F4aMyhw.jpeg'''
CODE | EXPLANATION |
---|---|
select() | to select all 'meta' elemets |
[<number>] | is to access a certain index number from a list of extracted elements (nodes) |
['content'] | to extract attribute from the HTML element (node) |
Scraping tables is an additional separate thing that can be done either with parsel
or bs4
web scraping libraries. However, pandas
simplifies this task a lot by providing a read_html()
method that can parse data from the <table>
.
Installation:
$ pip install pandas
A basic example of extracting table data from Wikipedia:
import pandas as pdtable = pd.read_html('https://en.wikipedia.org/wiki/History_of_Python')[0] # [0] = first tabledf = pd.DataFrame(data=table[['Latest micro version', 'Release date']]) # grabs 2 columns# df.set_index('Latest micro version', inplace=True) # drops default pandas DataFrame indexes, but can't be used in a for loopprint(df)for data in df['Latest micro version']: print(data)
Outputs:
Latest micro version Release date
0 0.9.9[2] 1991-02-20[2]
1 1.0.4[2] 1994-01-26[2]
2 1.1.1[2] 1994-10-11[2]
3 NaN 1995-04-13[2]
4 NaN 1995-10-13[2]
5 NaN 1996-10-25[2]
6 1.5.2[42] 1998-01-03[2]
7 1.6.1[42] 2000-09-05[43]
8 2.0.1[44] 2000-10-16[45]
9 2.1.3[44] 2001-04-15[46]
10 2.2.3[44] 2001-12-21[47]
11 2.3.7[44] 2003-06-29[48]
12 2.4.6[44] 2004-11-30[49]
13 2.5.6[44] 2006-09-19[50]
14 2.6.9[27] 2008-10-01[27]
15 2.7.18[32] 2010-07-03[32]
16 3.0.1[44] 2008-12-03[27]
17 3.1.5[52] 2009-06-27[52]
18 3.2.6[54] 2011-02-20[54]
19 3.3.7[55] 2012-09-29[55]
20 3.4.10[56] 2014-03-16[56]
21 3.5.10[58] 2015-09-13[58]
22 3.6.15[60] 2016-12-23[60]
23 3.7.13[61] 2018-06-27[61]
24 3.8.13[62] 2019-10-14[62]
25 3.9.14[63] 2020-10-05[63]
26 3.10.7[65] 2021-10-04[65]
27 3.11.0rc2[66] 2022-10-24[66]
28 NaN 2023-10[64]
29 Legend: Old versionOlder version, still maintainedLate...
30 Italics indicates the latest micro version of ... Italics indicates the latest micro version of ...
for
loop output:
0.9.9[2]
1.0.4[2]
1.1.1[2]
nan
nan
nan
1.5.2[42]
1.6.1[42]
2.0.1[44]
2.1.3[44]
2.2.3[44]
2.3.7[44]
2.4.6[44]
2.5.6[44]
2.6.9[27]
2.7.18[32]
3.0.1[44]
3.1.5[52]
3.2.6[54]
3.3.7[55]
3.4.10[56]
3.5.10[58]
3.6.15[60]
3.7.13[61]
3.8.13[62]
3.9.14[63]
3.10.7[65]
3.11.0rc2[66]
nan
Legend:
Italics indicates the latest micro version of currently supported versions as of 2022-07-11[needs update].
Keep in mind that those are just examples and additional data cleaning needs to be applied to make this data usable 🙂
Have a look at the gotchas that could happen with read_html()
:
Scraping with regular expression in Python is possible by re
module.
Why to scrape data with regular expressions in the first place?
There're a few main methods that could be used:
METHOD | PURPOSE |
---|---|
re.findall() | Returns a list of matches. To find all occurrences. |
re.search() | Returns a first match. To find the first occurrence. |
re.match() | To find match and the beginning of the string. search() vs match() |
group() | Returns one or more subgroups of the match. |
import redummy_text = '''Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscureLatin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature,discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance.The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.'''dates = re.findall(r'\d{1}\.\d{2}\.\d{2}', dummy_text) # https://regex101.com/r/VKYiA9/1years_bc = re.findall(r'\d+\s?\bBC\b', dummy_text) # https://regex101.com/r/ApypoB/1print(dates)print(years_bc)# ['1.10.32', '1.10.33', '1.10.32']# ['45 BC', '45 BC']
Here's a visualization of what is being matched by the regular expressions above:
The above regular expressions read like this:
In this section, we'll go over most popular Python web scraping tools that can extract data from static and dynamic websites.
There're a few Python web scraping packages/libraries to parse data from websites that are not JavaScript driven as such packages are designed to scrape data from static pages.
Parsel
is a library built to extract data from XML/HTML documents with XPath and CSS selectors support, and could be combined with regular expressions. It uses lxml
parser under the hood by default.
The great thing I like about parsel
(apart from XPath support) is that it returns None
if certain data is not present, so there's no need to create a lot of try/except
blocks to the same thing that looks ugly.
Installation:
$ pip install parsel
A few examples of extraction methods:
variable.css(".X5PpBb::text").get() # returns a text valuevariable.css(".gs_a").xpath("normalize-space()").get() # https://github.com/scrapy/parsel/issues/192#issuecomment-1042301716variable.css(".gSGphe img::attr(srcset)").get() # returns a attribute valuevariable.css(".I9Jtec::text").getall() # returns a list of strings valuesvariable.xpath('th/text()').get() # returns text value using xpath
CODE | EXPLANATION |
---|---|
css() | parse data from the passed CSS selector(s). Every CSS query traslates to XPath using csselect package under the hood. |
::text or ::attr(<attribute>) | extract textual or attribute data from the node. |
get() | get actual data returned from parsel |
getall() | get all a list of matches. |
.xpath('th/text()') | grabs textual data from <th> element |
Practical example using parsel
:
# https://serpapi.com/blog/scrape-naver-related-search-results-with-python/#full_codeimport requests, jsonfrom parsel import Selector# https://docs.python-requests.org/en/master/user/quickstart/#passing-parameters-in-urlsparams = { "query": "minecraft", # search query "where": "web" # web results. works with nexearch as well}# https://docs.python-requests.org/en/master/user/quickstart/#custom-headersheaders = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"}html = requests.get("https://search.naver.com/search.naver", params=params, headers=headers, timeout=30)selector = Selector(html.text)related_results = []# https://www.programiz.com/python-programming/methods/built-in/enumeratefor index, related_result in enumerate(selector.css(".related_srch .keyword"), start=1): keyword = related_result.css(".tit::text").get().strip() link = f'https://search.naver.com/search.naver{related_result.css("a::attr(href)").get()}' related_results.append({ "position": index, # 1,2,3.. "title": keyword, "link": link })print(json.dumps(related_results, indent=2, ensure_ascii=False))
BeautifulSoup
is also a library that build to extract data from HTML/XML documents. It's also could be combined with lxml
parser and also can be used in combo with regular expressions.
Unlike parsel
, BeautifulSoup
don't have support for XPath which would be pretty handy is some situations. Additionally, it lacks getall()
method that returns a list
of matches which is a shorthand of list comprehension, and it needs a lot of try/except
to handle missing data.
However, it can create new HTML nodes, for example, using wrap()
method or other methods for similar things.
It's very handy if parts of the data you want to extract are not properly structured e.g. HTML table without <table>...</table>
element.
You can create this element and then easily parse table data with pandas
read_html()
method.
Installation:
$ pip install bs4
A few examples of extraction methods using select()
and select_one()
:
variable.select('.gs_r.gs_or.gs_scl') # return a list of matchesvariable.select_one('.gs_rt').text # returns a single text value matchvariable.select_one('.gs_rt a')['href'] # returns a single attribute value match
Practical example using BeautifulSoup
:
# https://stackoverflow.com/a/71237540/15164646from bs4 import BeautifulSoupimport requests, lxmlparams = { "user": "VxOmZDgAAAAJ", # user-id, in this case Masatoshi Nei "hl": "en", # language "gl": "us", # country to search from "cstart": 0, # articles page. 0 is the first page "pagesize": "100" # articles per page}headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"}all_articles = []html = requests.post("https://scholar.google.com/citations", params=params, headers=headers, timeout=30)soup = BeautifulSoup(html.text, "lxml")for index, article in enumerate(soup.select("#gsc_a_b .gsc_a_t"), start=1): article_title = article.select_one(".gsc_a_at").text article_link = f'https://scholar.google.com{article.select_one(".gsc_a_at")["href"]}' article_authors = article.select_one(".gsc_a_at+ .gs_gray").text article_publication = article.select_one(".gs_gray+ .gs_gray").text all_articles.append({ "title": article_title, "link": article_link, "authors": article_authors, "publication": article_publication })
Browser automation is handy when you need to perform some sort of interaction with the webiste, for example, scrolling, clicks and similar things.
Such things could be done without browser automation, this is how we tend to do at SerpApi, however, it could be very complicated but on the flip side the reward is much faster data extraction.
playwright
is a modern alternative to selenium
. It can perform pretty much all interactions as the user would do i.e clicks, scrolls and many more.
Installation:
$ pip install playwright
Practial example of website interaction using playwright
and parsel
to extract the data.
The following script scrolls through all Google Play app reviews and then extract data:
# https://serpapi.com/blog/scrape-all-google-play-app-reviews-in-python/#full_codeimport time, json, refrom parsel import Selectorfrom playwright.sync_api import sync_playwrightdef run(playwright): page = playwright.chromium.launch(headless=True).new_page() page.goto("https://play.google.com/store/apps/details?id=com.collectorz.javamobile.android.books&hl=en_GB&gl=US") user_comments = [] # if "See all reviews" button present if page.query_selector('.Jwxk6d .u4ICaf button'): print("the button is present.") print("clicking on the button.") page.query_selector('.Jwxk6d .u4ICaf button').click(force=True) print("waiting a few sec to load comments.") time.sleep(4) last_height = page.evaluate('() => document.querySelector(".fysCi").scrollTop') # 2200 while True: print("scrolling..") page.keyboard.press("End") time.sleep(3) new_height = page.evaluate('() => document.querySelector(".fysCi").scrollTop') if new_height == last_height: break else: last_height = new_height selector = Selector(text=page.content()) page.close() print("done scrolling. Exctracting comments...") for index, comment in enumerate(selector.css(".RHo1pe"), start=1): user_comments.append({ "position": index, "user_name": comment.css(".X5PpBb::text").get(), "app_rating": re.search(r"\d+", comment.css(".iXRFPc::attr(aria-label)").get()).group(), "comment_date": comment.css(".bp9Aid::text").get(), "developer_comment": { "dev_title": comment.css(".I6j64d::text").get(), "dev_comment": comment.css(".ras4vb div::text").get(), "dev_comment_date": comment.css(".I9Jtec::text").get() } }) print(json.dumps(user_comments, indent=2, ensure_ascii=False))with sync_playwright() as playwright: run(playwright)
selenium
is very similar to playwright
but a little older with slightly different approaches of doing things.
$ pip install selenium
The following script performs a scroll until hits the very bottom of the Google Play search and then extracts each section with games:
from selenium import webdriverfrom selenium.webdriver.chrome.service import Servicefrom webdriver_manager.chrome import ChromeDriverManagerfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.support.wait import WebDriverWaitfrom selenium.webdriver.support import expected_conditions as ECfrom parsel import Selectorimport json, timegoogle_play_games = { 'Top charts': { 'Top free': [], 'Top grossing': [], 'Top paid': [] },}def scroll_page(url): service = Service(ChromeDriverManager().install()) options = webdriver.ChromeOptions() options.add_argument("--headless") options.add_argument("--lang=en") options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36") driver = webdriver.Chrome(service=service, options=options) driver.get(url) while True: try: scroll_button = driver.find_element(By.CSS_SELECTOR, '.snByac') driver.execute_script("arguments[0].click();", scroll_button) WebDriverWait(driver, 10000).until(EC.visibility_of_element_located((By.TAG_NAME, 'body'))) break except: driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") WebDriverWait(driver, 10000).until(EC.visibility_of_element_located((By.TAG_NAME, 'body'))) selector = Selector(driver.page_source) driver.quit() return selector def scrape_all_sections(selector): for section in selector.css('.Ubi8Z section'): section_title = section.css('.kcen6d span::text').get() time.sleep(2) google_play_games[section_title] = [] for game in section.css('.TAQqTe'): title = game.css('.OnEJge::text').get() link = 'https://play.google.com' + game.css('::attr(href)').get() category = game.css('.ubGTjb .sT93pb.w2kbF:not(.K4Wkre)::text').get() rating = game.css('.CKzsaf .w2kbF::text').get() rating = float(rating) if rating else None google_play_games[section_title].append({ 'title': title, 'link': link, 'category': category, 'rating': rating, }) print(json.dumps(google_play_games, indent=2, ensure_ascii=False)) def scrape_google_play_games(): params = { 'device': 'phone', 'hl': 'en_GB', # language 'gl': 'US', # country of the search } URL = f"https://play.google.com/store/games?device={params['device']}&hl={params['hl']}&gl={params['gl']}" result = scroll_page(URL) scrape_all_sections(result)if __name__ == "__main__": scrape_google_play_games()
scrapy
is a high-level webscraping framework designed to scrape data at scale and can be used to create a whole ETL pipeline.
However, you have to keep in mind that it's bulky, and could be quite confusing, and while it provides a lot of things for you, most of those things you may not need.
Installation:
$ pip install scrapy
Very simple scrapy
script:
import scrapyclass ScholarAuthorTitlesSpider(scrapy.Spider): name = 'google_scholar_author_titles' def scrapy_request(self): params = { "user": "cp-8uaAAAAAJ", # user-id "hl": "en", # language "gl": "us", # country to search from "cstart": 0, # articles page. 0 is the first page "pagesize": "100" # articles per page } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36" } yield scrapy.Request(url="https://scholar.google.com/citations", method="GET", headers=headers, meta=params, callback=self.parse) def parse(self, response): for index, article in enumerate(response.css("#gsc_a_b .gsc_a_t"), start=1): yield { "position": index, "title": article.css(".gsc_a_at::text").get(), "link": f'https://scholar.google.com{article.css(".gsc_a_at::attr(href)").get()}', "authors": article.css(".gsc_a_at+ .gs_gray::text").get(), "publication": article.css(".gs_gray+ .gs_gray::text").get() }
XHR request allows to talk to the server by making a request and getting data back in response. It's one of the first things that you can check before writing actual code. Those requests can be used to get data dircetly from website's "source" without the need to use parsing libraries/frameworks.
To find a certain XHR request you need:
F12
)If you find the request with the data you want, you can preview the data (example from the Poocoin.app):
When making XHR request, we need to pass URL parameters so that server can understand and "reply" to us.
Here's a simple illustration of that process:
To find those headers and URL query parameters, we need to go to a certain URL and look at Headers and Payload tabs and make sure we see what request method is used (GET
, POST
, etc).
We can do it like so:
From Insomnia (URL copied from the XHR->Headers tab).
📌Keep in mind that some of the passed URL parameters need to be scraped and passed to the URL beforehand (before making request to the server/api). URL can have some sort of a unique token or something different and can't be worked without it.
If the response is successful and you want to make an exact request in the script, those parameters could be automatically generated with tools such as Insomnia (or other alternatives) where you can test different types of requests with different parameters and headers.
Simple example but the same approach will be on other websites with or without passing URL parameters and headers:
import requests# https://requests.readthedocs.io/en/latest/user/quickstart/#json-response-contenthtml = requests.get('https://api.chucknorris.io/jokes/random').json()print(html['value'])# Once, due to an engine stall of his F-22 Raptor during a Dessert Storm sorte', Chuck Norris had to ejaculate over the Red Sea.
This is the next thing that could be checked after Dev Tools
-> XHR
. It's about looking at page source and trying to find the data there, that is either hidden in the rendered HTML or can't be scraped with selectors because it's being rendered by JavaScript.
One of the ways to find if there's the data you want is in the inline JSON or not:
CTRL + U
CTRL + F
, if some of the occurrences will be inside <script>
elements then congratulation, you found inline JSON or something similar 🙂Here's a visual example of how inline JSON could look like (from the TripAdvisor):
When we found that there's a data we want locates in the inline JSON, there're a few ways to extract it:
dict
.The following example is from our Google Arts & Culture - Artists blog post where we select()
all <script>
elements and then extract other data from matched element(s) using regular expressions:
from bs4 import BeautifulSoupimport requests, json, re, lxmlheaders = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36'}params = { 'tab': 'pop'}html = requests.get(f'https://artsandculture.google.com/category/artist', params=params, headers=headers, timeout=30)soup = BeautifulSoup(html.text, 'lxml') # 👇👇👇 data extractionall_script_tags = soup.select('script')# https://regex101.com/r/JQXUBm/1portion_of_script_tags = re.search('("stella\.common\.cobject",".*)\[\[\"A",\["stella\.pr","PrefixedAssets', str(all_script_tags)).group(1)# https://regex101.com/r/c9m9B0/1authors = re.findall(r'"stella\.common\.cobject","(.*?)","\d+', str(portion_of_script_tags))
CODE | EXPLANATION |
---|---|
re.findall() | Returns a list of matches. |
re.search() | Returns a first match. |
group() | Returns one or more subgroups of the match. |
The following example is from one of our Scrape Google Play Store App in Python blog posts.
The flow would be to also parse <script>
elements with regex and then turn it into dict
using json.loads()
:
json.loads( re.findall( r"<script nonce=\"\w+\" type=\"application/ld\+json\">({.*?)</script>", # regular expression str(soup.select("script")[11]), # input from where to search data re.DOTALL, # match any character: https://docs.python.org/3/library/re.html#re.DOTALL )[0] # access `list` from re.finall()) # convert to `dict` using json.loads()
After that, we can access it as a dictionary:
app_data["basic_info"]["name"] = basic_app_info.get("name")app_data["basic_info"]["type"] = basic_app_info.get("@type")app_data["basic_info"]["url"] = basic_app_info.get("url")
Full example:
from bs4 import BeautifulSoupimport requests, lxml, re, jsonheaders = { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"}params = { "id": "com.nintendo.zara", # app name "gl": "US", # country of the search "hl": "en_GB" # language of the search}# make a request and pass response to BeautifulSouphtml = requests.get("https://play.google.com/store/apps/details", params=params, headers=headers, timeout=30)soup = BeautifulSoup(html.text, "lxml")# where all app data will be storedapp_data = { "basic_info": {}}# 👇👇👇 data extraction# [11] index is a basic app information# https://regex101.com/r/zOMOfo/1basic_app_info = json.loads(re.findall(r"<script nonce=\"\w+\" type=\"application/ld\+json\">({.*?)</script>", str(soup.select("script")[11]), re.DOTALL)[0])app_data["basic_info"]["name"] = basic_app_info.get("name")app_data["basic_info"]["type"] = basic_app_info.get("@type")app_data["basic_info"]["url"] = basic_app_info.get("url")
The great examples of reverse engineering at our blog:
Make sure to check them both as here we're not going to duplicate the same information.
📌Information about Source and Application tabs is kinda introductory information as it's a big topics with a lot of steps to reproduce and it will be out of the scope of this blog post.
One of the approaches, when something complex needs to be extracted, could be done under the Source tab.
It could be done by degubbing website JS source code from certain files with debugger breakpoints (Dev tools
-> sources
-> debugger
) by trying to trace what is going on in the code and how can we intercept/create by ourselves data and use it the parser.
We've also done a SerpApi Podcast about scraping dynamic websites which is essentially the topic about using Source tab.
In this video Ilya told about his approach about investigating and reproducing Google Maps data URL parameter which defines data about certain Google Maps palace. This parameter hold data about place id, latitude and longitude.
A similar thing could be done in the Dev tools
-> Application
tab where we see, for example, cookies data and either intercept it on reverse engineer it by understanding how this cookie was built.
Ilya, one of the engineers at SerpApi has written in more detail about reverse engineering Location cookies from Walmart and his approach for such task.
1662479160
SwiftSoup
is a pure Swift library, cross-platform (macOS, iOS, tvOS, watchOS and Linux!), for working with real-world HTML. It provides a very convenient API for extracting and manipulating data, using the best of DOM, CSS, and jQuery-like methods. SwiftSoup
implements the WHATWG HTML5 specification, and parses HTML to the same DOM as modern browsers do.
SwiftSoup
is designed to deal with all varieties of HTML found in the wild; from pristine and validating, to invalid tag-soup; SwiftSoup
will create a sensible parse tree.>=2.0.0
Swift 4.2 1.7.4
SwiftSoup is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'SwiftSoup'
SwiftSoup is also available through Carthage. To install it, simply add the following line to your Cartfile:
github "scinfu/SwiftSoup"
SwiftSoup is also available through Swift Package Manager. To install it, simply add the dependency to your Package.Swift file:
...
dependencies: [
.package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.4.3"),
],
targets: [
.target( name: "YourTarget", dependencies: ["SwiftSoup"]),
]
...
pod try SwiftSoup
To parse an HTML document:
do {
let html = "<html><head><title>First parse</title></head>"
+ "<body><p>Parsed HTML into a doc.</p></body></html>"
let doc: Document = try SwiftSoup.parse(html)
return try doc.text()
} catch Exception.Error(let type, let message) {
print(message)
} catch {
print("error")
}
<p>Lorem <p>Ipsum
parses to <p>Lorem</p> <p>Ipsum</p>
)<td>Table data</td>
is wrapped into a <table><tr><td>...
)html
containing a head
and body
, and only appropriate elements within the head)Document
extends Element
extends Node.TextNode
extends Node
.Extract attributes, text, and HTML from elements
After parsing a document, and finding some elements, you'll want to get at the data inside those elements.
Node.attr(_ String key)
methodElement.text()
Element.html()
, or Node.outerHtml()
as appropriatedo {
let html: String = "<p>An <a href='http://example.com/'><b>example</b></a> link.</p>"
let doc: Document = try SwiftSoup.parse(html)
let link: Element = try doc.select("a").first()!
let text: String = try doc.body()!.text() // "An example link."
let linkHref: String = try link.attr("href") // "http://example.com/"
let linkText: String = try link.text() // "example"
let linkOuterH: String = try link.outerHtml() // "<a href="http://example.com/"><b>example</b></a>"
let linkInnerH: String = try link.html() // "<b>example</b>"
} catch Exception.Error(let type, let message) {
print(message)
} catch {
print("error")
}
The methods above are the core of the element data access methods. There are additional others:
Element.id()
Element.tagName()
Element.className()
and Element.hasClass(_ String className)
All of these accessor methods have corresponding setter methods to change the data.
Parse a document from a String
You have HTML in a Swift String, and you want to parse that HTML to get at its contents, or to make sure it's well formed, or to modify it. The String may have come from user input, a file, or from the web.
Use the static SwiftSoup.parse(_ html: String)
method, or SwiftSoup.parse(_ html: String, _ baseUri: String)
.
do {
let html = "<html><head><title>First parse</title></head>"
+ "<body><p>Parsed HTML into a doc.</p></body></html>"
let doc: Document = try SwiftSoup.parse(html)
return try doc.text()
} catch Exception.Error(let type, let message) {
print("")
} catch {
print("")
}
The parse(_ html: String, _ baseUri: String)
method parses the input HTML into a new Document
. The base URI argument is used to resolve relative URLs into absolute URLs, and should be set to the URL where the document was fetched from. If that's not applicable, or if you know the HTML has a base element, you can use the parse(_ html: String)
method.
As long as you pass in a non-null string, you're guaranteed to have a successful, sensible parse, with a Document containing (at least) a head
and a body
element.
Once you have a Document
, you can get at the data using the appropriate methods in Document
and its supers Element
and Node
.
Parsing a body fragment
You have a fragment of body HTML (e.g. div
containing a couple of p tags; as opposed to a full HTML document) that you want to parse. Perhaps it was provided by a user submitting a comment, or editing the body of a page in a CMS.
Use the SwiftSoup.parseBodyFragment(_ html: String)
method.
do {
let html: String = "<div><p>Lorem ipsum.</p>"
let doc: Document = try SwiftSoup.parseBodyFragment(html)
let body: Element? = doc.body()
} catch Exception.Error(let type, let message) {
print(message)
} catch {
print("error")
}
The parseBodyFragment
method creates an empty shell document, and inserts the parsed HTML into the body
element. If you used the normal SwiftSoup(_ html: String)
method, you would generally get the same result, but explicitly treating the input as a body fragment ensures that any bozo HTML provided by the user is parsed into the body
element.
The Document.body()
method retrieves the element children of the document's body
element; it is equivalent to doc.getElementsByTag("body")
.
If you are going to accept HTML input from a user, you need to be careful to avoid cross-site scripting attacks. See the documentation for the Whitelist
based cleaner, and clean the input with clean(String bodyHtml, Whitelist whitelist)
.
Sanitize untrusted HTML (to prevent XSS)
You want to allow untrusted users to supply HTML for output on your website (e.g. as comment submission). You need to clean this HTML to avoid cross-site scripting (XSS) attacks.
Use the SwiftSoup HTML Cleaner
with a configuration specified by a Whitelist
.
do {
let unsafe: String = "<p><a href='http://example.com/' onclick='stealCookies()'>Link</a></p>"
let safe: String = try SwiftSoup.clean(unsafe, Whitelist.basic())!
// now: <p><a href="http://example.com/" rel="nofollow">Link</a></p>
} catch Exception.Error(let type, let message) {
print(message)
} catch {
print("error")
}
A cross-site scripting attack against your site can really ruin your day, not to mention your users'. Many sites avoid XSS attacks by not allowing HTML in user submitted content: they enforce plain text only, or use an alternative markup syntax like wiki-text or Markdown. These are seldom optimal solutions for the user, as they lower expressiveness, and force the user to learn a new syntax.
A better solution may be to use a rich text WYSIWYG editor (like CKEditor or TinyMCE). These output HTML, and allow the user to work visually. However, their validation is done on the client side: you need to apply a server-side validation to clean up the input and ensure the HTML is safe to place on your site. Otherwise, an attacker can avoid the client-side Javascript validation and inject unsafe HMTL directly into your site
The SwiftSoup whitelist sanitizer works by parsing the input HTML (in a safe, sand-boxed environment), and then iterating through the parse tree and only allowing known-safe tags and attributes (and values) through into the cleaned output.
It does not use regular expressions, which are inappropriate for this task.
SwiftSoup provides a range of Whitelist
configurations to suit most requirements; they can be modified if necessary, but take care.
The cleaner is useful not only for avoiding XSS, but also in limiting the range of elements the user can provide: you may be OK with textual a
, strong
elements, but not structural div
or table
elements.
Cleaner
reference if you want to get a Document
instead of a String returnWhitelist
reference for the different canned options, and to create a custom whitelistSet attribute values
You have a parsed document that you would like to update attribute values on, before saving it out to disk, or sending it on as a HTTP response.
Use the attribute setter methods Element.attr(_ key: String, _ value: String)
, and Elements.attr(_ key: String, _ value: String)
.
If you need to modify the class attribute of an element, use the Element.addClass(_ className: String)
and Element.removeClass(_ className: String)
methods.
The Elements
collection has bulk attribute and class methods. For example, to add a rel="nofollow"
attribute to every a
element inside a div:
do {
try doc.select("div.comments a").attr("rel", "nofollow")
} catch Exception.Error(let type, let message) {
print(message)
} catch {
print("error")
}
Like the other methods in Element
, the attr methods return the current Element
(or Elements
when working on a collection from a select). This allows convenient method chaining:
do {
try doc.select("div.masthead").attr("title", "swiftsoup").addClass("round-box")
} catch Exception.Error(let type, let message) {
print(message)
} catch {
print("error")
}
Set the HTML of an element
You need to modify the HTML of an element.
Use the HTML setter methods in Element
:
do {
let doc: Document = try SwiftSoup.parse("<div>One</div><span>One</span>")
let div: Element = try doc.select("div").first()! // <div>One</div>
try div.html("<p>lorem ipsum</p>") // <div><p>lorem ipsum</p></div>
try div.prepend("<p>First</p>")
try div.append("<p>Last</p>")
print(div)
// now div is: <div><p>First</p><p>lorem ipsum</p><p>Last</p></div>
let span: Element = try doc.select("span").first()! // <span>One</span>
try span.wrap("<li><a href='http://example.com/'></a></li>")
print(doc)
// now: <html><head></head><body><div><p>First</p><p>lorem ipsum</p><p>Last</p></div><li><a href="http://example.com/"><span>One</span></a></li></body></html>
} catch Exception.Error(let type, let message) {
print(message)
} catch {
print("error")
}
Element.html(_ html: String)
clears any existing inner HTML in an element, and replaces it with parsed HTML.Element.prepend(_ first: String)
and Element.append(_ last: String)
add HTML to the start or end of an element's inner HTML, respectivelyElement.wrap(_ around: String)
wraps HTML around the outer HTML of an element.You can also use the Element.prependElement(_ tag: String)
and Element.appendElement(_ tag: String)
methods to create new elements and insert them into the document flow as a child element.
Setting the text content of elements
You need to modify the text content of an HTML document.
Solution
Use the text setter methods of Element
:
do {
let doc: Document = try SwiftSoup.parse("<div></div>")
let div: Element = try doc.select("div").first()! // <div></div>
try div.text("five > four") // <div>five > four</div>
try div.prepend("First ")
try div.append(" Last")
// now: <div>First five > four Last</div>
} catch Exception.Error(let type, let message) {
print(message)
} catch {
print("error")
}
The text setter methods mirror the [[HTML setter|Set the HTML of an element]] methods:
Element.text(_ text: String)
clears any existing inner HTML in an element, and replaces it with the supplied text.Element.prepend(_ first: String)
and Element.append(_ last: String)
add text nodes to the start or end of an element's inner HTML, respectively The text should be supplied unencoded: characters like <
, >
etc will be treated as literals, not HTML.Use DOM methods to navigate a document
You have a HTML document that you want to extract data from. You know generally the structure of the HTML document.
Use the DOM-like methods available after parsing HTML into a Document
.
do {
let html: String = "<a id=1 href='?foo=bar&mid<=true'>One</a> <a id=2 href='?foo=bar<qux&lg=1'>Two</a>"
let els: Elements = try SwiftSoup.parse(html).select("a")
for link: Element in els.array() {
let linkHref: String = try link.attr("href")
let linkText: String = try link.text()
}
} catch Exception.Error(let type, let message) {
print(message)
} catch {
print("error")
}
Elements provide a range of DOM-like methods to find elements, and extract and manipulate their data. The DOM getters are contextual: called on a parent Document they find matching elements under the document; called on a child element they find elements under that child. In this way you can window in on the data you want.
getElementById(_ id: String)
getElementsByTag(_ tag:String)
getElementsByClass(_ className: String)
getElementsByAttribute(_ key: String)
(and related methods)siblingElements()
, firstElementSibling()
, lastElementSibling()
, nextElementSibling()
, previousElementSibling()
parent()
, children()
, child(_ index: Int)
Element data
attr(_ key: Strin)
to get and attr(_ key: String, _ value: String)
to set attributesattributes()
to get all attributesid()
, className()
and classNames()
text()
to get and text(_ value: String)
to set the text contenthtml()
to get and html(_ value: String)
to set the inner HTML contentouterHtml()
to get the outer HTML valuedata()
to get data content (e.g. of script and style tags)tag()
and tagName()
append(_ html: String)
, prepend(html: String)
appendText(text: String)
, prependText(text: String)
appendElement(tagName: String)
, prependElement(tagName: String)
html(_ value: String)
Use selector syntax to find elements
You want to find or manipulate elements using a CSS or jQuery-like selector syntax.
Use the Element.select(_ selector: String)
and Elements.select(_ selector: String)
methods:
do {
let doc: Document = try SwiftSoup.parse("...")
let links: Elements = try doc.select("a[href]") // a with href
let pngs: Elements = try doc.select("img[src$=.png]")
// img with src ending .png
let masthead: Element? = try doc.select("div.masthead").first()
// div with class=masthead
let resultLinks: Elements? = try doc.select("h3.r > a") // direct a after h3
} catch Exception.Error(let type, let message) {
print(message)
} catch {
print("error")
}
SwiftSoup elements support a CSS (or jQuery) like selector syntax to find matching elements, that allows very powerful and robust queries.
The select
method is available in a Document
, Element
, or in Elements
. It is contextual, so you can filter by selecting from a specific element, or by chaining select calls.
Select returns a list of Elements
(as Elements
), which provides a range of methods to extract and manipulate the results.
tagname
: find elements by tag, e.g. a
ns|tag
: find elements by tag in a namespace, e.g. fb|name
finds <fb:name>
elements#id
: find elements by ID, e.g. #logo
.class
: find elements by class name, e.g. .masthead
[attribute]
: elements with attribute, e.g. [href]
[^attr]
: elements with an attribute name prefix, e.g. [^data-]
finds elements with HTML5 dataset attributes[attr=value]
: elements with attribute value, e.g. [width=500]
(also quotable, like [data-name='launch sequence']
)[attr^=value]
, [attr$=value]
, [attr*=value]
: elements with attributes that start with, end with, or contain the value, e.g. [href*=/path/]
[attr~=regex]
: elements with attribute values that match the regular expression; e.g. img[src~=(?i)\.(png|jpe?g)]
*
: all elements, e.g. *
el#id
: elements with ID, e.g. div#logo
el.class
: elements with class, e.g. div.masthead
el[attr]
: elements with attribute, e.g. a[href]
a[href].highlight
child
: child elements that descend from ancestor, e.g. .body p
finds p
elements anywhere under a block with class "body"parent > child
: child elements that descend directly from parent, e.g. div.content > p
finds p elements; and body > *
finds the direct children of the body tagsiblingA + siblingB
: finds sibling B element immediately preceded by sibling A, e.g. div.head + div
siblingA ~ siblingX
: finds sibling X element preceded by sibling A, e.g. h1 ~ p
el
, el
, el
: group multiple selectors, find unique elements that match any of the selectors; e.g. div.masthead
, div.logo
:lt(n)
: find elements whose sibling index (i.e. its position in the DOM tree relative to its parent) is less than n; e.g. td:lt(3)
:gt(n)
: find elements whose sibling index is greater than n; e.g. div p:gt(2)
:eq(n)
: find elements whose sibling index is equal to n; e.g. form input:eq(1)
:has(seletor)
: find elements that contain elements matching the selector; e.g. div:has(p)
:not(selector)
: find elements that do not match the selector; e.g. div:not(.logo)
:contains(text)
: find elements that contain the given text. The search is case-insensitive; e.g. p:contains(swiftsoup)
:containsOwn(text)
: find elements that directly contain the given text:matches(regex)
: find elements whose text matches the specified regular expression; e.g. div:matches((?i)login)
:matchesOwn(regex)
: find elements whose own text matches the specified regular expressionExamples
let html = "<html><head><title>First parse</title></head><body><p>Parsed HTML into a doc.</p></body></html>"
guard let doc: Document = try? SwiftSoup.parse(html) else { return }
guard let elements = try? doc.getAllElements() else { return html }
for element in elements {
for textNode in element.textNodes() {
[...]
}
}
try doc.head()?.append("<style>html {font-size: 2em}</style>")
let html = "<div class=\"container-fluid\">"
+ "<div class=\"panel panel-default \">"
+ "<div class=\"panel-body\">"
+ "<form id=\"coupon_checkout\" action=\"http://uat.all.com.my/checkout/couponcode\" method=\"post\">"
+ "<input type=\"hidden\" name=\"transaction_id\" value=\"4245\">"
+ "<input type=\"hidden\" name=\"lang\" value=\"EN\">"
+ "<input type=\"hidden\" name=\"devicetype\" value=\"\">"
+ "<div class=\"input-group\">"
+ "<input type=\"text\" class=\"form-control\" id=\"coupon_code\" name=\"coupon\" placeholder=\"Coupon Code\">"
+ "<span class=\"input-group-btn\">"
+ "<button class=\"btn btn-primary\" type=\"submit\">Enter Code</button>"
+ "</span>"
+ "</div>"
+ "</form>"
+ "</div>"
+ "</div>"
guard let doc: Document = try? SwiftSoup.parse(html) else { return } // parse html
let elements = try doc.select("[name=transaction_id]") // query
let transaction_id = try elements.get(0) // select first element
let value = try transaction_id.val() // get value
print(value) // 4245
guard let doc: Document = try? SwiftSoup.parse(html) else { return } // parse html
guard let txt = try? doc.text() else { return }
print(txt)
let xml = "<?xml version='1' encoding='UTF-8' something='else'?><val>One</val>"
guard let doc = try? SwiftSoup.parse(xml, "", Parser.xmlParser()) else { return }
guard let element = try? doc.getElementsByTag("val").first() else { return } // Find first element
try element.text("NewValue") // Edit Value
let valueString = try element.text() // "NewValue"
<img src>
do {
let doc: Document = try SwiftSoup.parse(html)
let srcs: Elements = try doc.select("img[src]")
let srcsStringArray: [String?] = srcs.array().map { try? $0.attr("src").description }
// do something with srcsStringArray
} catch Exception.Error(_, let message) {
print(message)
} catch {
print("error")
}
href
of <a>
let html = "<a id=1 href='?foo=bar&mid<=true'>One</a> <a id=2 href='?foo=bar<qux&lg=1'>Two</a>"
guard let els: Elements = try? SwiftSoup.parse(html).select("a") else { return }
for element: Element in els.array() {
print(try? element.attr("href"))
}
Output:
"?foo=bar&mid<=true"
"?foo=bar<qux&lg=1"
let text = "Hello &<> Å å π 新 there ¾ © »"
print(Entities.escape(text))
print(Entities.unescape(text))
print(Entities.escape(text, OutputSettings().encoder(String.Encoding.ascii).escapeMode(Entities.EscapeMode.base)))
print(Entities.escape(text, OutputSettings().charset(String.Encoding.ascii).escapeMode(Entities.EscapeMode.extended)))
print(Entities.escape(text, OutputSettings().charset(String.Encoding.ascii).escapeMode(Entities.EscapeMode.xhtml)))
print(Entities.escape(text, OutputSettings().charset(String.Encoding.utf8).escapeMode(Entities.EscapeMode.extended)))
print(Entities.escape(text, OutputSettings().charset(String.Encoding.utf8).escapeMode(Entities.EscapeMode.xhtml)))
Output:
"Hello &<> Å å π 新 there ¾ © »"
"Hello &<> Å å π 新 there ¾ © »"
"Hello &<> Å å π 新 there ¾ © »"
"Hello &<> Å å π 新 there ¾ © »"
"Hello &<> Å å π 新 there ¾ © »"
"Hello &<> Å å π 新 there ¾ © »"
"Hello &<> Å å π 新 there ¾ © »"
Nabil Chatbi, scinfu@gmail.com
SwiftSoup was ported to Swift from Java Jsoup library.
SwiftSoup is available under the MIT license. See the LICENSE file for more info.
Author: scinfu
Source code: https://github.com/scinfu/SwiftSoup
License: MIT license
#swift
1661953820
IP Geolocalization using the Geolite2 Database
Installation
The package is registered in the General registry and so can be installed at the REPL with
julia> using Pkg
julia> Pkg.add("GeoIP")
Usage
You can use MaxMind geolite2 csv files downloaded from the site. Due to the MaxMind policy, GeoLite.jl
does not distribute GeoLite2
files and does not provide download utilities. For automated download it is recommended to use MaxMind GeoIP Update program. For proper functioning of GeoIP.jl
you need to download GeoLite2 City
datafile, usually it should have a name like GeoLite2-City-CSV_20191224.zip
.
Files processing and loading provided with load()
call. Directory where data is located should be located either in ENV["GEOIP_DATADIR"]
or it can be passed as an argument to load
function. Zip file location can be passed as an argument or it can be stored in ENV["GEOIP_ZIPFILE"]
. For example
using GeoIP
geodata = load(zipfile = "GeoLite2-City-CSV_20191224.zip", datadir = "/data")
If ENV["GEOIP_DATADIR"]
is set to "/data"
and ENV["GEOIP_ZIPFILE"]
is set to "GeoLite2-City-CSV_20191224.zip"
then it is equivalent to
using GeoIP
geodata = load()
You can get the ip data with the geolocate
function or by using []
using GeoIP
geodata = load(zipfile = "GeoLite2-City-CSV_20191224.zip")
geolocate(geodata, ip"1.2.3.4") # returns dictionary with all relevant information
# Equivalent to
geodata[ip"1.2.3.4"]
# Equivalent, but slower version
geodata["1.2.3.4"]
geolocate
form is useful for broadcasting
geolocate.(geodata, [ip"1.2.3.4", ip"8.8.8.8"]) # returns vector of geo data.
It is possible to use localized version of geo files. To load localized data, one can use locales
argument of the load
function. To switch between different locales is possible with the help of setlocale
function.
using GeoIP
geodata = load(zipfile = "GeoLite2-City-CSV_20191224.zip", locales = [:en, :fr])
geodata[ip"201.186.185.1"]
# Dict{String, Any} with 21 entries:
# "time_zone" => "America/Santiago"
# "subdivision_2_name" => missing
# "accuracy_radius" => 100
# "geoname_id" => 3874960
# "continent_code" => "SA"
# "postal_code" => missing
# "continent_name" => "South America"
# "locale_code" => "en"
# "subdivision_2_iso_code" => missing
# "location" => Location(-72.9436, -41.4709, 0.0, "WGS84")
# "v4net" => IPv4Net("201.186.185.0/24")
# "subdivision_1_name" => "Los Lagos Region"
# "subdivision_1_iso_code" => "LL"
# "city_name" => "Port Montt"
# "metro_code" => missing
# "registered_country_geoname_id" => 3895114
# "is_in_european_union" => 0
# "is_satellite_provider" => 0
# "is_anonymous_proxy" => 0
# "country_name" => "Chile"
# "country_iso_code" => "CL"
geodata_fr = setlocale(geodata, :fr)
geodata_fr[ip"201.186.185.1"]
# Dict{String, Any} with 21 entries:
# "time_zone" => "America/Santiago"
# "subdivision_2_name" => missing
# "accuracy_radius" => 100
# "geoname_id" => 3874960
# "continent_code" => "SA"
# "postal_code" => missing
# "continent_name" => "Amérique du Sud"
# "locale_code" => "fr"
# "subdivision_2_iso_code" => missing
# "location" => Location(-72.9436, -41.4709, 0.0, "WGS84")
# "v4net" => IPv4Net("201.186.185.0/24")
# "subdivision_1_name" => missing
# "subdivision_1_iso_code" => "LL"
# "city_name" => "Puerto Montt"
# "metro_code" => missing
# "registered_country_geoname_id" => 3895114
# "is_in_european_union" => 0
# "is_satellite_provider" => 0
# "is_anonymous_proxy" => 0
# "country_name" => "Chili"
# "country_iso_code" => "CL"
During load
procedure, it is possible to use either Symbol
notation, i.e. locales = [:en, :fr]
or one can pass Vector
of Pair
, where first argument is the locale name and second argument is a regular expression, which defines the name of the CSV file, which contains necessary localization. For example locales = [:en => r"Locations-en.csv%", :fr => r"Locations-fr.csv"]
. By default, following locales are supported :en, :de, :ru, :ja, :es, :fr, :pt_br, :zh_cn
.
Default locale, which is used in getlocale
response can be set with the help of deflocale
argument of the load
function. For example, to get :fr
locale by default
geodata = load(zipfile = "GeoLite2-City-CSV_20191224.zip", locales = [:en, :fr], deflocale = :fr)
Acknowledgements
This product uses, but not include, GeoLite2 data created by MaxMind, available from http://www.maxmind.com.
Author: JuliaWeb
Source Code: https://github.com/JuliaWeb/GeoIP.jl
License: View license
1660692780
A Combined Flutter library & reusable code for use
Run this command:
With Flutter:
$ flutter pub add code_id_storage
This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get
):
dependencies:
code_id_storage: ^0.0.8
Alternatively, your editor might support flutter pub get
. Check the docs for your editor to learn more.
Now in your Dart code, you can use:
import 'package:code_id_storage/code_id_storage.dart';
This package is use for internal code.id Company
For how to use you can contact me. To know to use it. ]
Original article source at: https://pub.dev/packages/code_id_storage
1659997920
Lean database - it's a simple-database, it's just an rpc
-based server with the basic Get/Set/Delete operations. The database can ingest any value
and store it at a key
, it only supports rpc, via gRPC
. The encoded binary message is stored, and served without touching the value on the server-side. To that end it is mostly a gRPC
-cache, but I intend it to be a more general building block.
The database is operating on "key"-level only. If you need secondary indexes you needed to maintain two versions of the data or actually create the index (id'
-id
-mapping) table yourself. Some key-value databases offer more solutions than this; this does not, and will not, offering too many solutions most often lead to poorer solutions in general.
There is no Query language to clutter your code! I know, awesome, right?!
This project started out as a learning project for myself, to learn golang
, rpc
and gRPC
.
The project is written in golang
. It will be packaged as a scratch
-container (linux amd64
). I will not support other ways of downloading. As always you can simply go build
images are mjuul/ld:<tag>
and (alpine)mjuul/ld:<tag>-client
. There is also a standalone client container mjuul/ld-client
.
The container mjuul/ld:<tag>
is just a scratch container with the Linux/amd64 image as entrypoint.
The container mjuul/ld:<tag>-client
is based on the image mjuul/ld-client
adding the binary ld
to it and running that at startup. The client serves as an interactive shell for the database, see client.
This project exposes badgerDB. You should be able to use the badgerDB CLI-tool on the database.
Hashmap Get-Set-Delete semantics! With bidirectional streaming rpc's. No lists, because aggregation of data should be kept at a minimum. The APIs for get and delete further implement unidirectional server-side streams for querying via KeyRange
.
I consider the gRPC api to be feature complete. While the underlying implementation may change to enable better database configuration and/or usage of this code as a library. Maturity may also bring changes to the server implementation.
See test for a client implementations, the testing package builds on the data from DMI - Free data initiative (specifically the lightning data set), but can easily be changed to ingest other data, ingestion and read separated into two different clients.
The API is expandable. Because of how gRPC encoding works you can replace the bytes
type value
tag on the client side with whatever you want. This way you could use it to store dynamically typed objects using Any
. Or you can save and query the database with a fixed or reflected type.
The test folder holds two small programs that implements a fixed type: my_message.proto.
The client uses reflection to serialize/deserialize json to a message given a .proto
-file.
CRUD operations must be implemented client side, use Get -> [decision] -> Set
to implement create or update, the way you want to. fx
Create Get -> if {empty response} -> Set
Update Get/Delete -> if {non-empty} -> [map?] -> Set
To have done this server side would cause so much friction. All embeddable key-value databases, to my knowledge, implement Get-Set-Delete semantics, so whether you go with bolt/bbolt or badger you would always end up having this friction; so naturally you implement it without CRUD-semantics. Implementing a concurrent GetMany
/SetMany
ping-pong client-service feels a lot more elegant anyways.
via flags or environment variables:
flag ENV default description
------------------------------------------------------------------------------------
-port PORT 5326 "5326" spells out "lean" in T9 keyboards
-db-location DB_LOCATION ld_badger The folder location where badger stores its database-files
-in-mem IN_MEM false save data in memory (or not) setting this to true ignores db-location.
-log-level LOG_LEVEL INFO the logging level of the server
The container mjuul/ld:<tag>-client
does not support flags for ld
, use environment variables. (Since it is ld-client
that is the entrypoint)
ProfaneDB
uses field options to find your object's key, and can ingest a list (repeated), your key can be composite, and you don't have to think about your key. (I envy the design a bit (it's shiny), but then again I don't feel like that is the best design).
ld
forces you to design your key, and force single-object(no-aggregation/non-repeated) thinking.
ProfaneDB
does not support any type of non-singular key queries; you will have to build query objects with very high knowledge of your keys (specific keys). This may force you to make fewer keys, and do more work in the client. (you may end up searching for a needle in a haystack, or completely loosing a key)
ld
supports KeyRange
s, you can then make very specific keys, and more of them, and think about the key-design, and query that via, From
, To
, Prefix
and/or Pattern
syntax.
ProfaneDB
uses an inbuilt extension for its .proto
. pro: you can use their .proto
file as is. con: google's Any-type is just like map, and requires the implementer to send type-knowledge on each object on the wire.
ld
use the underlying protocol buffers encoding design, con: this force the implementer to edit their .proto
file, which is an anti-pattern. pro: while the database will not know anything about the value it saves, the type will be packed binary and can be serialised.
ld
support bulk operations (via stream methods) natively. ProfaneDB
via a repeated nested object, Memory-wise, streaming is preferred.
Author: MikkelHJuul
Source Code: https://github.com/MikkelHJuul/ld
License: Unlicense license
1659153600
CSS:has()
伪类多年来一直是最受期待的特性之一。这是一个 4 级 CSS 选择器,现在作为Chrome 105及更高版本中完全受支持的功能提供,并且很可能很快也会成为其他浏览器的常规功能。
CSS 中的:has()
in CSS 是一个关系伪类,允许您检查给定元素是否包含某些子元素,如果找到任何匹配项,则选择它,然后相应地对其进行样式设置。
本文解释了:has()
选择器的需求、它的一般用法、从简单到高级的各种应用程序和用例、浏览器兼容性以及后备。
:has()
需要选择器?大多数开发人员依赖 JavaScript 来实现 CSS 默认不支持的某些功能。然而,今天的网络浏览器比以往任何时候都更强大,这为许多新的和有趣的 CSS 功能打开了大门。
选择:has()
器就是这样一种功能。它适用于父元素而不是子元素,并使用逗号分隔的选择器列表作为参数,然后在它所代表的元素的子元素中查找匹配项。它的功能类似于 jQueryhas()
方法。
让我们回顾一下前端开发中选择器的一些一般需求,这些需求:has()
解释了为什么多年来对这个特性的需求如此之高。
首先是检查一个元素是否包含某些其他元素的可能性,然后相应地对其进行样式设置。JavaScript 是以前唯一的方法,你可以在这里看到:
let content = document.querySelector("#content"),
headings = []
if(content) {
headings = content.querySelectorAll("h1, h2, h3")
}
if(headings.length) {
// do something
}
在上面的代码中,我们使用querySelectorAll
了 ,这是一种 Web API 方法来检查标题的划分元素。该方法返回所提供元素的 NodeList。
一个空的 NodeList 意味着直接父级中不存在任何元素。上面的实现可以在这里找到。或者,您可以在此处查看 CSS:has()
替代方案。
其次是从子元素中选择父元素的能力。同样,由于缺乏 CSS 工具,开发人员依赖于 Web API 的 parentNode 属性:
let el = document.querySelector(".someElement"),
elParent = null
if(el) {
elParent = el.parentNode
}
在此处查看 上面代码的 CodePen 演示,以及选择器如何在:has()
此处实现。
最后,可以选择给定元素的前一个兄弟。CSS 中的兄弟选择器允许您选择下一个兄弟,但没有 CSS 唯一的方法来选择以前的兄弟。JavaScript 实现如下所示(在此处查看 CodePen 演示):
let el = document.querySelector(".someElement"),
elPs = null
if(el) {
elPs = el.previousElementSibling
}
您可以在:has()
此处查看如何使用选择器执行此操作。
我们上面用 JavaScript 所做的一切现在都可以用 CSS 来实现:has()
。在了解如何在 Google Chrome 上启用和测试此功能后,我们将在下一节中一一介绍。
:has()
选择器的支持如果您使用的是较旧的 Chrome 版本 (v.101-104),则可以从 Chrome 标志启用此功能。确保您拥有 Chrome 101+ 并从浏览器的地址栏中导航到 chrome://flags。
将Experimental Web Platform features设置为Enabled,一切顺利。:has()
重新启动浏览器,您就可以在 Chrome 浏览器中使用 CSS 。
:has()
选择器有什么作用?让我们探讨一下:has()
伪类、它的用法和属性。因为它是一个伪类,所以它可以用冒号附加到任何选择器,并接受类、ID 和 HTML 标记作为参数。
下面的代码解释了它的一般用法和语法。只有当它包含使用伪类作为参数传递给它的元素时,该类.selector
才会被选中:has()
.selector:has(.class) { ... }
.selector:has(#id) { ... }
.selector:has(div) { ... }
只要您认为合适,您就可以一个接一个地链接多个:has()
伪类。下面的代码演示了这种链接是如何工作的:
.selector:has(div):has(.class):has(#id) {
...
}
您还可以提供多个元素选择器的列表,类似于链接,但效率更高:
.selector:has(div, .class, #id) {
...
}
:has()
假设您不小心为伪类提供了无效的元素选择器。忽略它并只考虑有效的选择器是足够聪明的:
.selector:has(div, accordion, .class, ::lobster, #id) {
...
}
accordion
并且::lobster
是无效的选择器,在上述情况下将被忽略。您不会在开发人员工具中看到任何 CSS 错误或警报。
让我们看一下 CSS:has()
选择器派上用场的一些示例场景。
这可能是:has()
选择器最常见的用途,因为它的默认行为是选择包含特定元素集的某些内容。但是如果我们只知道子元素呢?
通用选择器 ( *
) 可以在这里与:has()
子组合器 ( ) 一起使用,>
以快速选择父级,甚至不知道它的任何内容。
如上面属性部分所述,:has()
允许您传递多个实体的列表,这意味着您可以在给定元素中检查任意数量的选择器。
通过将 CSS 相邻兄弟组合器与:has()
伪类组合,可以选择前一个兄弟。
您可能已经知道,相邻兄弟组合器选择给定元素的下一个兄弟。我们可以使用这种行为has()
来获取上一个兄弟。:has()
简单地说,如果一个元素有下一个兄弟,用和组合器很容易选择它+
!
使用选择器可以避免单独设置带有和不带有某些子元素的样式:has()
。带有和不带标题的图形元素是这里的完美示例。
如果一个figcaption
元素存在,但不包含任何文本怎么办?在这种情况下,我们可以使用:not
and:empty
选择器来检查内容。
与图形装饰类似,我们可以轻松切换多段引用的文本对齐方式。在这里查看它的实际应用。
我们已经有一个伪类,用于调用:empty
不包含任何内容的样式元素。就针对空状态而言,它无法按预期工作。内部的一个空间足以让它将一个元素识别为非空。
:empty
在这种情况下使用并不理想。让我们创建一个卡片网格,其中还包含一些无内容卡片。我们将使用选择器设置空卡片状态的样式:has()
。
在为文章设计样式时,保持类型和块元素对齐是一项棘手的工作。考虑一个将代码块、图形和块引用与通用类型元素混合的示例。
块元素应该有更多的垂直间距和装饰,以在不同的印刷实体中脱颖而出。这里有一些 CSS 来处理这个问题。
p {
margin: 0;
}
p:not(:last-child) {
margin-bottom: 1.5em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.3;
margin: 1.5em 0 1.5rem;
color: #111;
}
pre,
figure,
blockquote {
margin: 3em 0;
}
figcaption {
margin-top: 1em;
font-size: 0.75em;
color: #888;
text-align: center;
}
上面的代码片段会在标题和块元素之间产生不均匀的垂直间距,如此处所示。但是可以使用之前的兄弟选择技巧来弥补这些违规行为。
假设您要在 CSS 中创建具有两种变体的 CTA 或按钮组件:第一个是默认的普通按钮,第二个是带有图标的。
现在,如果您打算为此编写两个单独的类,则可以使用:has()
选择器轻松避免。问题是简单地检查.btn
元素的.btn-icon
子元素,然后相应地设置样式。
假设标题组件有两种布局变化:一种是固定宽度,另一种是流动的,当然。
为了将标题的内容保持在固定宽度内,我们必须在其中添加一个内容包装器元素。这两个标头版本的标记之间的唯一区别是包装元素。
然后我们可以将:has()
with:not()
选择器配对并添加 CSS 类,如下面的演示所示。
调整布局的另一个示例可以是在网格中的列数达到一定数量后立即对其进行修改。
当您不使用minmax()
确定最佳宽度以适合其网格中的列的功能时,这可能会很方便。
正如您在上面看到的,一旦超过两个项目的标记,网格就会自动调整为三列。
当交互性与反馈正确连接时,交互性得到了最好的定义。对于交互式 HTML 表单,向用户提供有关其输入的反馈是您能做的最好的事情。
在、 和伪类的帮助下:has()
,我们可以使我们的表单更加动态,并且不涉及 JavaScript。:valid:invalid
如果没有:has()
找到对选择器的支持,上面讨论的示例会显示一个错误对话框。这可以使用@supports
CSS 规则来完成,如下面的代码片段所示:
@supports (selector(:has(*))) {
.selector:has(...) {
...
}
}
以上是根据需要检查元素的支持和样式的渐进式方法。如果代码库直接使用了一些新特性,这里有一种方法可以编写相同的向后兼容版本:
@supports not (selector(:has(*))) {
.selector {
...
}
}
如果没有找到支持,同样也可以用来提示用户。
您还可以使用 JavaScript 中的 Support API 来检测浏览器对不同功能的支持。:has()
这是一个纯粹在 JavaScript中检查支持的示例:
if(!CSS.supports('selector(html:has(body))'))) { // if not supported
// Use the conventional JavaScript ways to emulate :has()
}
在上面的文章中,我们了解:has()
了 4 级 CSS 选择器。我们涵盖了它的需求,它可以在您的前端项目中替换 JavaScript 的一些情况,以及一般到高级用例。
我们还讨论了不同浏览器提供给它的当前支持,并学习了如何检查浏览器支持。
感谢您一直阅读。我希望你通过这个学到新的东西。随时在评论中分享更多:has()
有用的示例。
来源:https ://blog.logrocket.com/advanced-guide-css-has-selector/
1659153600
La pseudo-classe CSS :has()
est l'une des fonctionnalités les plus attendues depuis des années. Il s'agit d'un sélecteur CSS de niveau 4, désormais disponible en tant que fonctionnalité entièrement prise en charge dans Chrome 105 et ultérieur , et qui deviendra probablement bientôt une fonctionnalité régulière sur d'autres navigateurs également.
Le :has()
in CSS est une pseudo-classe relationnelle qui vous permet de vérifier si un élément donné contient certains éléments enfants, de le sélectionner si une correspondance est trouvée, puis de le styler en conséquence.
Cet article explique la nécessité du :has()
sélecteur, son utilisation générale, diverses applications et cas d'utilisation du plus simple au plus avancé, la compatibilité du navigateur et les solutions de secours.
:has()
sélecteur est-il nécessaire ?La plupart des développeurs s'appuient sur JavaScript pour certaines fonctionnalités que CSS ne prend pas en charge par défaut. Cependant, les navigateurs Web d'aujourd'hui sont plus puissants que jamais, ce qui ouvre la porte à de nombreuses fonctionnalités CSS nouvelles et intéressantes.
Le :has()
sélecteur est l'une de ces fonctionnalités. Il fonctionne sur le parent plutôt que sur les enfants et utilise une liste de sélecteurs séparés par des virgules comme arguments, puis recherche des correspondances parmi les enfants de l'élément qu'il représente. Sa fonctionnalité est similaire à la has()
méthode jQuery.
Passons en revue certains des besoins généraux du :has()
sélecteur dans le développement frontal qui expliquent pourquoi cette fonctionnalité était si demandée depuis des années.
Le premier est la possibilité de vérifier si un élément contient certains autres éléments ou non, puis de le styliser en conséquence. JavaScript était le seul moyen de le faire auparavant, comme vous pouvez le voir ici :
let content = document.querySelector("#content"),
headings = []
if(content) {
headings = content.querySelectorAll("h1, h2, h3")
}
if(headings.length) {
// do something
}
Dans le code ci-dessus, nous avons utilisé querySelectorAll
, qui est une méthode d'API Web pour vérifier un élément de division pour les en-têtes. La méthode renvoie une NodeList des éléments fournis.
Une NodeList vide signifierait qu'il n'y a aucun élément présent dans le parent immédiat. Une implémentation de ce qui précède peut être trouvée ici . Ou, vous pouvez voir l' alternative CSS:has()
ici .
La deuxième est la possibilité de sélectionner l'élément parent parmi les enfants. Encore une fois, en l'absence d'une installation CSS pour le faire, les développeurs se sont appuyés sur la propriété parentNode de l'API Web :
let el = document.querySelector(".someElement"),
elParent = null
if(el) {
elParent = el.parentNode
}
Voir une démo CodePen du code ci-dessus ici , et comment le :has()
sélecteur l'implémente ici .
Enfin, il est possible de sélectionner le frère précédent d'un élément donné. Le sélecteur de frère dans CSS vous permet de sélectionner le frère suivant, mais il n'y avait pas de moyen uniquement CSS de sélectionner les frères et sœurs précédents. L'implémentation de JavaScript ressemble à ceci ( voir une démo CodePen ici ) :
let el = document.querySelector(".someElement"),
elPs = null
if(el) {
elPs = el.previousElementSibling
}
Vous pouvez voir comment effectuer cette action avec le :has()
sélecteur ici .
Tout ce que nous avons fait ci-dessus avec JavaScript peut maintenant être réalisé avec CSS :has()
. Nous les couvrirons un par un dans la section suivante après avoir appris comment activer et tester cette fonctionnalité sur Google Chrome.
:has()
sélecteurSi vous utilisez une ancienne version de Chrome (v.101-104), vous pouvez activer cette fonctionnalité à partir des indicateurs Chrome. Assurez-vous que vous disposez de Chrome 101+ et accédez à chrome://flags dans la barre d'adresse du navigateur.
Définissez les fonctionnalités de la plate-forme Web expérimentale sur Activé et vous êtes prêt à partir. Relancez le navigateur et vous pourrez travailler avec CSS :has()
dans votre navigateur Chrome.
:has()
?Explorons la :has()
pseudo-classe, son utilisation et ses propriétés. Comme il s'agit d'une pseudo-classe, elle peut être attachée à n'importe quel sélecteur avec deux-points et accepte les classes, les ID et les balises HTML comme paramètres.
Le code ci-dessous explique son utilisation générale et sa syntaxe. La classe .selector
n'est sélectionnée que si elle contient les éléments qui lui sont passés en tant que paramètres à l'aide de la has()
pseudo-classe :
.selector:has(.class) { ... }
.selector:has(#id) { ... }
.selector:has(div) { ... }
Vous pouvez enchaîner plusieurs :has()
pseudo-classes les unes après les autres quand bon vous semble. Le code ci-dessous montre comment fonctionne ce chaînage :
.selector:has(div):has(.class):has(#id) {
...
}
Vous pouvez également fournir une liste de plusieurs sélecteurs d'éléments, similaire au chaînage, mais beaucoup plus efficace :
.selector:has(div, .class, #id) {
...
}
Supposons que vous ayez accidentellement fourni un sélecteur d'élément invalide à la :has()
pseudo-classe. Il est assez intelligent pour ignorer cela et ne considérer que les sélecteurs valides :
.selector:has(div, accordion, .class, ::lobster, #id) {
...
}
accordion
et ::lobster
sont des sélecteurs invalides et seront ignorés dans le cas ci-dessus. Vous ne verrez aucune erreur ou alerte CSS dans les outils de développement.
Jetons un coup d'œil à quelques exemples de scénarios dans lesquels le :has()
sélecteur CSS est utile.
C'est peut-être l'utilisation la plus courante du :has()
sélecteur, car son comportement par défaut consiste à sélectionner quelque chose s'il contient un ensemble spécifique d'éléments. Mais que se passe-t-il si nous ne connaissons que l'élément enfant ?
Le sélecteur universel ( *
) peut être utilisé ici avec :has()
et le combinateur enfant ( >
) pour sélectionner rapidement le parent sans même rien savoir à ce sujet.
Comme indiqué dans le segment des propriétés ci-dessus, :has()
vous permet de transmettre une liste de plusieurs entités, ce qui signifie que vous pouvez rechercher autant de sélecteurs que vous le souhaitez dans un élément donné.
La sélection du frère précédent est rendue possible en combinant le combinateur de frère adjacent CSS avec la :has()
pseudo-classe.
Comme vous le savez peut-être déjà, le combinateur de frères adjacents sélectionne le frère suivant d'un élément donné. Nous pouvons utiliser ce comportement avec has()
pour obtenir le frère précédent. En termes simples, si un élément a un frère suivant, il est facile de le sélectionner avec un :has()
combinateur+
!
Le style des choses avec et sans certains éléments enfants séparément peut être évité avec le :has()
sélecteur. L'élément de figure avec et sans légende en est l'exemple parfait ici.
Que faire si un figcaption
élément est là, mais ne contient pas de texte ? Dans ce cas, nous pouvons utiliser les sélecteurs :not
et :empty
pour contrôler le contenu.
Semblable à la décoration des figures, nous pouvons facilement changer l'alignement du texte des guillemets avec plus d'un paragraphe. Voyez-le en action ici .
Nous avons déjà une pseudo-classe appelée :empty
pour les éléments de style qui ne contiennent rien. En ce qui concerne le ciblage d'un état vide, cela ne fonctionne pas comme prévu. Un espace à l'intérieur suffit pour qu'il reconnaisse un élément comme non vide.
Ce n'est pas idéal à utiliser :empty
dans ce cas. Créons une grille de cartes qui contient également quelques cartes sans contenu. Nous allons styliser les états de la carte vide avec le :has()
sélecteur.
Lors du style d'un article, maintenir l'alignement des éléments de type et de bloc est une tâche délicate. Prenons un exemple de mélange de blocs de code, de chiffres et de guillemets avec les éléments de type général.
Les éléments de bloc doivent avoir plus d'espacement vertical et de décorations pour se démarquer des différentes entités typographiques. Voici quelques CSS pour gérer cela.
p {
margin: 0;
}
p:not(:last-child) {
margin-bottom: 1.5em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.3;
margin: 1.5em 0 1.5rem;
color: #111;
}
pre,
figure,
blockquote {
margin: 3em 0;
}
figcaption {
margin-top: 1em;
font-size: 0.75em;
color: #888;
text-align: center;
}
L'extrait ci-dessus produira un espacement vertical irrégulier entre les titres et les éléments de bloc, comme illustré ici. Mais il est possible de compenser ces irrégularités en utilisant le précédent hack de sélection des frères et sœurs.
Supposons que vous créez un CTA ou un composant de bouton en CSS avec deux variantes : la première est le bouton simple par défaut et la seconde est avec une icône.
Maintenant, si vous envisagez d'écrire deux classes distinctes pour cela, cela peut être facilement évité avec le :has()
sélecteur. Le problème consiste simplement à vérifier l' .btn
élément pour un .btn-icon
enfant, puis à le styler en conséquence.
Disons que vous avez deux variantes de mise en page pour un composant d'en-tête : l'une est à largeur fixe et l'autre est liquide, bien sûr.
Pour conserver le contenu de l'en-tête dans une largeur fixe, nous devons ajouter un élément wrapper de contenu à l'intérieur. La seule différence entre le balisage de ces deux versions d'en-tête est l'élément wrapper.
Nous pouvons ensuite associer le sélecteur :has()
with :not()
et ajouter les classes CSS, comme indiqué dans la démonstration ci-dessous.
Un autre exemple d'ajustement de la disposition peut être de modifier le nombre de colonnes dans une grille dès qu'elles atteignent un certain nombre.
Cela peut être pratique lorsque vous n'utilisez pas la minmax()
fonction qui détermine la meilleure largeur pour ajuster une colonne dans sa grille.
Comme vous pouvez le voir ci-dessus, la grille s'ajuste automatiquement à trois colonnes dès qu'elle dépasse la marque de deux éléments.
L'interactivité est mieux définie lorsqu'elle est correctement connectée au feedback. En ce qui concerne les formulaires HTML interactifs, offrir à l'utilisateur des commentaires sur sa saisie est la meilleure chose que vous puissiez faire.
Avec l'aide des pseudo-classes :has()
, :valid
et :invalid
, nous pouvons rendre nos formulaires beaucoup plus dynamiques, et sans JavaScript impliqué.
Les exemples discutés ci-dessus affichent une boîte de dialogue d'erreur si aucun support pour le :has()
sélecteur n'est trouvé. Cela peut être fait en utilisant la @supports
règle CSS, comme indiqué dans les extraits de code ci-dessous :
@supports (selector(:has(*))) {
.selector:has(...) {
...
}
}
Ci-dessus se trouve la manière progressive de vérifier le support et de styliser l'élément selon les besoins. Si une base de code utilise directement certaines nouvelles fonctionnalités, voici un moyen d'écrire les versions rétrocompatibles de celles-ci :
@supports not (selector(:has(*))) {
.selector {
...
}
}
La même chose peut également être utilisée pour avertir l'utilisateur si aucun support n'a été trouvé.
Vous pouvez également utiliser l'API de support en JavaScript pour détecter la prise en charge par le navigateur de différentes fonctionnalités. Voici un exemple de vérification de la :has()
prise en charge uniquement en JavaScript :
if(!CSS.supports('selector(html:has(body))'))) { // if not supported
// Use the conventional JavaScript ways to emulate :has()
}
Dans l'article ci-dessus, nous avons découvert :has()
, un sélecteur CSS de niveau 4. Nous avons couvert ses besoins, certains cas où il peut remplacer JavaScript dans vos projets frontend, et des cas d'utilisation généraux à avancés.
Nous avons également discuté de la prise en charge actuelle offerte par différents navigateurs et appris comment vérifier la prise en charge du navigateur.
Merci d'avoir lu jusqu'au bout. J'espère que vous avez appris quelque chose de nouveau grâce à cela. N'hésitez pas à partager plus d'exemples de ce :has()
qui peut être utile dans les commentaires.
Source : https://blog.logrocket.com/advanced-guide-css-has-selector/
1659151800
A pseudoclasse CSS :has()
tem sido um dos recursos mais esperados há anos. É um seletor de CSS de nível 4, agora disponível como um recurso totalmente compatível no Chrome 105 em diante e provavelmente se tornará um recurso regular em breve também em outros navegadores.
O :has()
in CSS é uma pseudo-classe relacional que permite verificar se um determinado elemento contém certos elementos filho, selecioná-lo se houver alguma correspondência e, em seguida, estilize-o de acordo.
Este artigo explica a necessidade do :has()
seletor, seu uso geral, vários aplicativos e casos de uso do simples ao avançado, compatibilidade do navegador e os fallbacks.
:has()
seletor é necessário?A maioria dos desenvolvedores depende do JavaScript para certos recursos que o CSS não suporta por padrão. No entanto, os navegadores da web hoje estão mais poderosos do que nunca, o que está abrindo portas para muitos recursos CSS novos e interessantes.
O :has()
seletor é um desses recursos. Ele funciona no pai em vez de nos filhos e usa uma lista de seletores separados por vírgulas como argumentos e, em seguida, procura correspondências entre os filhos do elemento que está representando. Sua funcionalidade é semelhante ao has()
método jQuery.
Vamos examinar algumas das necessidades gerais do :has()
seletor no desenvolvimento de front-end que explicam por que esse recurso teve uma demanda tão alta por anos.
A primeira é a possibilidade de verificar se um elemento contém certos outros elementos ou não e, em seguida, estilizá-lo de acordo. JavaScript era a única maneira de fazer isso antes, como você pode ver aqui:
let content = document.querySelector("#content"),
headings = []
if(content) {
headings = content.querySelectorAll("h1, h2, h3")
}
if(headings.length) {
// do something
}
No código acima, usamos querySelectorAll
, que é um método de API da Web para verificar um elemento de divisão para títulos. O método retorna um NodeList dos elementos fornecidos.
Um NodeList vazio significaria que não há elementos presentes no pai imediato. Uma implementação do acima pode ser encontrada aqui . Ou você pode ver a alternativa CSS:has()
aqui .
A segunda é a capacidade de selecionar o elemento pai dos filhos. Novamente, na falta de um recurso CSS para fazer isso, os desenvolvedores confiaram na propriedade parentNode da API da Web:
let el = document.querySelector(".someElement"),
elParent = null
if(el) {
elParent = el.parentNode
}
Veja uma demonstração do CodePen do código acima aqui e como o :has()
seletor implementa isso aqui .
Por fim, há a possibilidade de selecionar o irmão anterior de um determinado elemento. O seletor de irmãos em CSS permite que você selecione o próximo irmão, mas não havia uma maneira somente de CSS de selecionar irmãos anteriores. A implementação do JavaScript se parece com isso ( veja uma demonstração do CodePen aqui ):
let el = document.querySelector(".someElement"),
elPs = null
if(el) {
elPs = el.previousElementSibling
}
Você pode ver como realizar esta ação com o :has()
seletor aqui .
Tudo o que fizemos acima com JavaScript agora pode ser feito com CSS :has()
. Vamos abordá-los um por um na próxima seção depois de aprender como ativar e testar esse recurso no Google Chrome.
:has()
seletorSe você estiver em uma versão mais antiga do Chrome (v.101-104), poderá ativar esse recurso nos sinalizadores do Chrome. Certifique-se de ter o Chrome 101+ e navegue até chrome://flags na barra de endereços do navegador.
Defina os recursos da Plataforma Web Experimental como Ativado e pronto. Reinicie o navegador e você poderá trabalhar com CSS :has()
em seu navegador Chrome.
:has()
Vamos explorar a :has()
pseudoclasse, seu uso e propriedades. Por ser uma pseudo-classe, ela pode ser anexada a qualquer seletor com dois pontos e aceita classes, IDs e tags HTML como parâmetros.
O código abaixo explica seu uso geral e sintaxe. A classe .selector
só é selecionada se contiver os elementos passados a ela como parâmetros usando a has()
pseudoclasse:
.selector:has(.class) { ... }
.selector:has(#id) { ... }
.selector:has(div) { ... }
Você pode encadear várias :has()
pseudo-classes uma após a outra sempre que achar melhor. O código abaixo demonstra como esse encadeamento funciona:
.selector:has(div):has(.class):has(#id) {
...
}
Você também pode fornecer uma lista de seletores de vários elementos, semelhante ao encadeamento, mas muito mais eficiente:
.selector:has(div, .class, #id) {
...
}
Suponha que você acidentalmente forneceu um seletor de elemento inválido para a :has()
pseudoclasse. É inteligente o suficiente para ignorar isso e considerar apenas os seletores válidos:
.selector:has(div, accordion, .class, ::lobster, #id) {
...
}
accordion
e ::lobster
são seletores inválidos e serão ignorados no caso acima. Você não verá erros ou alertas de CSS nas ferramentas do desenvolvedor.
Vamos dar uma olhada em alguns cenários de exemplo em que o :has()
seletor de CSS é útil.
Esse pode ser o uso mais comum do :has()
seletor, pois seu comportamento padrão é selecionar algo se ele contiver um conjunto específico de elementos. Mas e se estivermos cientes apenas do elemento filho?
O seletor universal ( *
) pode ser usado aqui com :has()
e o combinador filho ( >
) para selecionar rapidamente o pai sem saber nada sobre isso.
Conforme discutido no segmento de propriedades acima, :has()
permite passar uma lista de várias entidades, o que significa que você pode verificar quantos seletores desejar em um determinado elemento.
A seleção do irmão anterior é possível combinando o combinador irmão adjacente CSS com a :has()
pseudo-classe.
Como você já deve saber, o combinador de irmãos adjacentes seleciona o próximo irmão de um determinado elemento. Podemos usar esse comportamento com has()
para obter o irmão anterior. Simplificando, se um elemento tem um próximo irmão, é fácil selecioná-lo com um :has()
combinador +
!
Estilizar coisas com e sem certos elementos filho separadamente pode ser evitado com o :has()
seletor. O elemento de figura com e sem legenda é o exemplo perfeito aqui.
E se um figcaption
elemento estiver lá, mas não contiver nenhum texto? Nesse caso, podemos usar os seletores :not
e para verificar o conteúdo.:empty
Semelhante à decoração de figuras, podemos alternar facilmente o alinhamento do texto das citações em bloco com mais de um parágrafo. Veja-o em ação aqui .
Já temos uma pseudo-classe chamada :empty
para elementos de estilo que não contêm nada. No que diz respeito ao direcionamento de um estado vazio, ele não funciona conforme o esperado. Um espaço interno é suficiente para reconhecer um elemento como não vazio.
Não é ideal para usar :empty
neste caso. Vamos criar uma grade de cartões que também contém alguns cartões sem conteúdo. Vamos estilizar os estados vazios do cartão com o :has()
seletor.
Ao estilizar um artigo, manter os elementos de tipo e bloco alinhados é uma tarefa complicada. Considere um exemplo de mistura de blocos de código, figuras e cotações de bloco com os elementos de tipo geral.
Os elementos do bloco devem ter mais espaçamento vertical e decorações para se destacar entre as diferentes entidades tipográficas. Aqui está algum CSS para lidar com isso.
p {
margin: 0;
}
p:not(:last-child) {
margin-bottom: 1.5em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.3;
margin: 1.5em 0 1.5rem;
color: #111;
}
pre,
figure,
blockquote {
margin: 3em 0;
}
figcaption {
margin-top: 1em;
font-size: 0.75em;
color: #888;
text-align: center;
}
O snippet acima produzirá espaçamento vertical desigual entre títulos e elementos de bloco, conforme mostrado aqui. Mas é possível compensar essas irregularidades usando o hack de seleção de irmãos anterior.
Suponha que você esteja criando um CTA ou componente de botão em CSS com duas variações: a primeira é o botão padrão, simples, e a segunda é com um ícone.
Agora, se você planeja escrever duas classes separadas para isso, isso pode ser facilmente evitado com o :has()
seletor. O problema é simplesmente verificar o .btn
elemento para um .btn-icon
filho e, em seguida, estilizá-lo de acordo.
Digamos que você tenha duas variações de layout para um componente de cabeçalho: uma é de largura fixa e a outra é líquida, é claro.
Para manter o conteúdo do cabeçalho dentro de uma largura fixa, temos que adicionar um elemento wrapper de conteúdo dentro dele. A única diferença entre a marcação dessas duas versões de cabeçalho é o elemento wrapper.
Podemos então emparelhar o seletor :has()
com :not()
e adicionar as classes CSS, conforme mostrado na demonstração abaixo.
Outro exemplo de ajuste de layout pode ser a modificação do número de colunas em uma grade assim que atingirem um determinado número.
Isso pode ser útil quando você não estiver usando a minmax()
função que determina a melhor largura para ajustar uma coluna em sua grade.
Como você pode ver acima, a grade se ajusta automaticamente para três colunas assim que ultrapassa a marca de dois itens.
A interatividade é melhor definida quando está devidamente conectada ao feedback. Quando se trata de formulários HTML interativos, oferecer feedback ao usuário sobre sua entrada é a melhor coisa que você pode fazer.
Com a ajuda das classes :has()
, :valid
, e :invalid
pseudo-classes, podemos tornar nossos formulários muito mais dinâmicos e sem JavaScript envolvido.
Os exemplos discutidos acima mostram uma caixa de diálogo de erro se nenhum suporte para o :has()
seletor for encontrado. Isso pode ser feito usando a @supports
regra CSS, conforme mostrado nos trechos de código abaixo:
@supports (selector(:has(*))) {
.selector:has(...) {
...
}
}
Acima está a maneira progressiva de verificar o suporte e estilizar o elemento conforme necessário. Se uma base de código usa alguns novos recursos diretamente, aqui está uma maneira de escrever as versões compatíveis com versões anteriores do mesmo:
@supports not (selector(:has(*))) {
.selector {
...
}
}
O mesmo também pode ser usado para avisar o usuário se nenhum suporte foi encontrado.
Você também pode usar a API de suporte em JavaScript para detectar o suporte do navegador para diferentes recursos. Aqui está um exemplo dessa verificação de :has()
suporte puramente em JavaScript:
if(!CSS.supports('selector(html:has(body))'))) { // if not supported
// Use the conventional JavaScript ways to emulate :has()
}
No artigo acima, aprendemos sobre :has()
, um seletor CSS de nível 4. Cobrimos suas necessidades, alguns casos em que ele pode substituir o JavaScript em seus projetos de front-end e casos de uso gerais a avançados.
Também discutimos o suporte atual oferecido a ele por diferentes navegadores e aprendemos como verificar o suporte do navegador.
Obrigado por ler todo o caminho. Espero que você tenha aprendido algo novo com isso. Sinta-se à vontade para compartilhar mais exemplos de como :has()
pode ser útil nos comentários.
Fonte: https://blog.logrocket.com/advanced-guide-css-has-selector/
1659150000
La pseudoclase CSS :has()
ha sido una de las características más esperadas durante años. Es un selector de CSS de nivel 4, ahora disponible como una función totalmente compatible en Chrome 105 en adelante , y probablemente también se convertirá en una función regular pronto en otros navegadores.
El :has()
CSS es una pseudoclase relacional que le permite verificar si un elemento dado contiene ciertos elementos secundarios, seleccionarlo si se encuentra alguna coincidencia y luego diseñarlo en consecuencia.
Este artículo explica la necesidad del :has()
selector, su uso general, varias aplicaciones y casos de uso, desde simples hasta avanzados, la compatibilidad del navegador y las alternativas.
:has()
necesario el selector?La mayoría de los desarrolladores confían en JavaScript para ciertas funciones que CSS no admite de forma predeterminada. Sin embargo, los navegadores web de hoy en día son más potentes que nunca, lo que abre las puertas a muchas funciones CSS nuevas e interesantes.
El :has()
selector es una de esas características. Funciona en el padre en lugar de en los hijos, y usa una lista de selectores separados por comas como argumentos, luego busca coincidencias entre los hijos del elemento que representa. Su funcionalidad es similar al has()
método jQuery.
Repasemos algunas de las necesidades generales del :has()
selector en el desarrollo frontend que explican por qué esta función tuvo una demanda tan alta durante años.
Primero está la posibilidad de verificar si un elemento contiene ciertos otros elementos o no, y luego diseñarlo en consecuencia. JavaScript era la única forma de hacer esto antes, como puede ver aquí:
let content = document.querySelector("#content"),
headings = []
if(content) {
headings = content.querySelectorAll("h1, h2, h3")
}
if(headings.length) {
// do something
}
En el código anterior, hemos utilizado querySelectorAll
, que es un método de la API web para verificar los encabezados de un elemento de división. El método devuelve una lista de nodos de los elementos proporcionados.
Una NodeList vacía significaría que no hay elementos presentes en el padre inmediato. Una implementación de lo anterior se puede encontrar aquí . O bien, puede ver la alternativa de CSS:has()
aquí .
En segundo lugar está la capacidad de seleccionar el elemento principal de los elementos secundarios. Una vez más, a falta de una función de CSS para hacerlo, los desarrolladores se han basado en la propiedad parentNode de Web API:
let el = document.querySelector(".someElement"),
elParent = null
if(el) {
elParent = el.parentNode
}
Vea una demostración de CodePen del código anterior aquí , y cómo el :has()
selector implementa esto aquí .
Finalmente, existe la posibilidad de seleccionar el hermano anterior de un elemento dado. El selector de hermanos en CSS le permite seleccionar al siguiente hermano, pero no había una forma de CSS solo para seleccionar hermanos anteriores. La implementación de JavaScript se ve así ( ver una demostración de CodePen aquí ):
let el = document.querySelector(".someElement"),
elPs = null
if(el) {
elPs = el.previousElementSibling
}
Puedes ver cómo realizar esta acción con el :has()
selector aquí .
Todo lo que hicimos anteriormente con JavaScript ahora se puede lograr con CSS :has()
. Los cubriremos uno por uno en la siguiente sección después de aprender cómo habilitar y probar esta función en Google Chrome.
:has()
selectorSi tiene una versión anterior de Chrome (v.101-104), puede habilitar esta función desde las banderas de Chrome. Asegúrate de tener Chrome 101+ y navega hasta chrome://flags desde la barra de direcciones del navegador.
Establezca las características de la plataforma web experimental en Habilitado y listo. Vuelva a iniciar el navegador y podrá trabajar con CSS :has()
en su navegador Chrome.
:has()
Exploremos la :has()
pseudoclase, su uso y propiedades. Debido a que es una pseudoclase, se puede adjuntar a cualquier selector con dos puntos y acepta clases, ID y etiquetas HTML como parámetros.
El siguiente código explica su uso general y sintaxis. La clase .selector
solo se selecciona si contiene los elementos que se le pasan como parámetros usando la has()
pseudoclase:
.selector:has(.class) { ... }
.selector:has(#id) { ... }
.selector:has(div) { ... }
Puede encadenar varias :has()
pseudoclases una tras otra cuando lo considere oportuno. El siguiente código demuestra cómo funciona este encadenamiento:
.selector:has(div):has(.class):has(#id) {
...
}
También puede proporcionar una lista de selectores de elementos múltiples, similar al encadenamiento, pero mucho más eficiente:
.selector:has(div, .class, #id) {
...
}
Suponga que accidentalmente proporcionó un selector de elementos no válido a la :has()
pseudoclase. Es lo suficientemente inteligente como para ignorar eso y considerar solo los selectores válidos:
.selector:has(div, accordion, .class, ::lobster, #id) {
...
}
accordion
y ::lobster
son selectores no válidos y se ignorarán en el caso anterior. No verá errores de CSS ni alertas en las herramientas para desarrolladores.
Echemos un vistazo a algunos escenarios de ejemplo en los que el :has()
selector de CSS resulta útil.
Este puede ser el uso más común del :has()
selector, porque su comportamiento predeterminado es seleccionar algo si contiene un conjunto específico de elementos. Pero, ¿y si solo somos conscientes del elemento hijo?
El selector universal ( *
) se puede usar aquí con :has()
el combinador secundario ( >
) para seleccionar rápidamente el principal sin siquiera saber nada al respecto.
Como se discutió en el segmento de propiedades anterior, :has()
le permite pasar una lista de múltiples entidades, lo que significa que puede verificar tantos selectores como desee dentro de un elemento dado.
La selección del hermano anterior es posible al combinar el combinador de hermanos adyacentes de CSS con la :has()
pseudoclase.
Como ya sabrá, el combinador de hermanos adyacentes selecciona al siguiente hermano de un elemento dado. Podemos usar este comportamiento con has()
para obtener el hermano anterior. En pocas palabras, si un elemento tiene un hermano, ¡es fácil seleccionarlo con un :has()
combinador +
!
El estilo de las cosas con y sin ciertos elementos secundarios por separado se puede evitar con el :has()
selector. El elemento de figura con y sin título es el ejemplo perfecto aquí.
¿Qué pasa si un figcaption
elemento está ahí, pero no contiene ningún texto? En ese caso, podemos usar los selectores :not
y :empty
para controlar el contenido.
Similar a la decoración de figuras, podemos cambiar fácilmente la alineación del texto de las citas en bloque con más de un párrafo. Véalo en acción aquí .
Ya tenemos una pseudoclase llamada :empty
para diseñar elementos que no contienen nada. En lo que respecta a apuntar a un estado vacío, no funciona como se esperaba. Un espacio dentro es suficiente para que reconozca un elemento como no vacío.
No es ideal para usar :empty
en este caso. Vamos a crear una cuadrícula de tarjetas que también contenga algunas tarjetas sin contenido. Diseñaremos los estados de tarjeta vacíos con el :has()
selector.
Al diseñar un artículo, mantener alineados los elementos de tipo y bloque es un trabajo complicado. Considere un ejemplo de combinación de bloques de código, figuras y comillas de bloque con los elementos de tipo general.
Los elementos de bloque deben tener más espaciado vertical y decoraciones para destacarse entre las diferentes entidades tipográficas. Aquí hay algo de CSS para manejar eso.
p {
margin: 0;
}
p:not(:last-child) {
margin-bottom: 1.5em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.3;
margin: 1.5em 0 1.5rem;
color: #111;
}
pre,
figure,
blockquote {
margin: 3em 0;
}
figcaption {
margin-top: 1em;
font-size: 0.75em;
color: #888;
text-align: center;
}
El fragmento anterior producirá un espacio vertical desigual entre los encabezados y los elementos del bloque, como se muestra aquí. Pero es posible compensar estas irregularidades utilizando el truco de selección de hermanos anterior.
Suponga que está creando un CTA o componente de botón en CSS con dos variaciones: la primera es el botón simple predeterminado y la segunda es con un icono.
Ahora, si planea escribir dos clases separadas para eso, puede evitarlo fácilmente con el :has()
selector. El truco es simplemente verificar el .btn
elemento para un .btn-icon
niño y luego diseñarlo en consecuencia.
Digamos que tiene dos variaciones de diseño para un componente de encabezado: uno es de ancho fijo y el otro es líquido, por supuesto.
Para mantener el contenido del encabezado dentro de un ancho fijo, debemos agregar un elemento contenedor de contenido dentro de él. La única diferencia entre el marcado de estas dos versiones de encabezado es el elemento contenedor.
Luego podemos emparejar el selector :has()
con :not()
y agregar las clases de CSS, como se muestra en la siguiente demostración.
Otro ejemplo de ajuste del diseño puede ser modificar el número de columnas en una cuadrícula tan pronto como alcancen un número determinado.
Esto puede ser útil cuando no está utilizando la minmax()
función que determina el mejor ancho para que quepa una columna en su cuadrícula.
Como puede ver arriba, la cuadrícula se ajusta automáticamente a tres columnas tan pronto como excede la marca de dos elementos.
La interactividad se define mejor cuando está correctamente conectada a la retroalimentación. Cuando se trata de formularios HTML interactivos, lo mejor que puede hacer es ofrecer comentarios al usuario sobre su entrada.
Con la ayuda de las pseudoclases :has()
, :valid
y :invalid
, podemos hacer que nuestros formularios sean mucho más dinámicos y sin JavaScript involucrado.
Los ejemplos discutidos anteriormente muestran un cuadro de diálogo de error si no :has()
se encuentra soporte para el selector. Esto se puede hacer usando la @supports
regla CSS, como se muestra en los siguientes fragmentos de código:
@supports (selector(:has(*))) {
.selector:has(...) {
...
}
}
Arriba está la forma progresiva de verificar el soporte y diseñar el elemento según sea necesario. Si un código base usa algunas características nuevas directamente, aquí hay una forma de escribir las versiones compatibles con versiones anteriores del mismo:
@supports not (selector(:has(*))) {
.selector {
...
}
}
Lo mismo también se puede usar para avisar al usuario si no se encontró soporte.
También puede usar la API de soporte en JavaScript para detectar el soporte del navegador para diferentes funciones. Aquí hay un ejemplo de esa verificación de :has()
soporte puramente en JavaScript:
if(!CSS.supports('selector(html:has(body))'))) { // if not supported
// Use the conventional JavaScript ways to emulate :has()
}
En el artículo anterior, aprendimos sobre :has()
, un selector de CSS de nivel 4. Cubrimos sus necesidades, algunos casos en los que puede reemplazar JavaScript en sus proyectos frontend y casos de uso generales a avanzados.
También discutimos el soporte actual que le ofrecen diferentes navegadores y aprendimos cómo verificar el soporte del navegador.
Gracias por leer hasta el final. Espero que hayas aprendido algo nuevo a través de esto. Siéntase libre de compartir más ejemplos de cómo :has()
puede ser útil en los comentarios.
Fuente: https://blog.logrocket.com/advanced-guide-css-has-selector/
1659146400
CSS:has()
疑似クラスは、何年もの間最も待望されていた機能の1つです。これはレベル4のCSSセレクターであり、 Chrome 105以降で完全にサポートされる機能として利用できるようになり、他のブラウザーでもまもなく通常の機能になる可能性があります。
in CSSはリレーショナル疑似クラスであり、:has()
特定の要素に特定の子要素が含まれているかどうかを確認し、一致する要素が見つかった場合はそれを選択して、それに応じてスタイルを設定できます。
この記事では、:has()
セレクターの必要性、その一般的な使用法、単純なものから高度なものまでのさまざまなアプリケーションとユースケース、ブラウザーの互換性、およびフォールバックについて説明します。
:has()
セレクターが必要なのですか?ほとんどの開発者は、CSSがデフォルトでサポートしていない特定の機能をJavaScriptに依存しています。ただし、今日のWebブラウザーはこれまでになく強力であり、多くの新しく興味深いCSS機能への扉を開いています。
:has()
セレクターはそのような機能の1つです。子ではなく親で機能し、セレクターのコンマ区切りリストを引数として使用して、それが表す要素の子の間で一致するものを探します。その機能はjQueryhas()
メソッドに似ています。
:has()
フロントエンド開発におけるセレクターの一般的なニーズのいくつかを調べて、この機能が何年にもわたって非常に高い需要があった理由を説明しましょう。
1つは、要素に他の特定の要素が含まれているかどうかを確認し、それに応じてスタイルを設定する可能性です。あなたがここで見ることができるように、JavaScriptは以前これをする唯一の方法でした:
let content = document.querySelector("#content"),
headings = []
if(content) {
headings = content.querySelectorAll("h1, h2, h3")
}
if(headings.length) {
// do something
}
上記のコードquerySelectorAll
では、見出しの分割要素をチェックするためのWebAPIメソッドであるを使用しました。このメソッドは、指定された要素のNodeListを返します。
空のNodeListは、直接の親に要素が存在しないことを意味します。上記の実装はここにあります。または、 CSSの:has()
代替案をここで確認できます。
2つ目は、子から親要素を選択する機能です。繰り返しになりますが、そうするためのCSS機能がないため、開発者はWebAPIのparentNodeプロパティに依存しています。
let el = document.querySelector(".someElement"),
elParent = null
if(el) {
elParent = el.parentNode
}
上記のコードのCodePenデモと、セレクターがこれを実装する方法:has()
をここで表示し ます。
最後に、特定の要素の前の兄弟を選択する機能があります。CSSの兄弟セレクターを使用すると、次の兄弟を選択できますが、前の兄弟を選択するCSSのみの方法はありませんでした。JavaScriptの実装は次のようになります(CodePenのデモはこちらからご覧ください)。
let el = document.querySelector(".someElement"),
elPs = null
if(el) {
elPs = el.previousElementSibling
}
セレクターを使用してこのアクションを実行する方法については、:has()
こちらをご覧ください。
上記でJavaScriptを使用して行ったすべてのことは、CSSを使用して実現できるようになりました:has()
。Google Chromeでこの機能を有効にしてテストする方法を学習した後、次のセクションでそれらを1つずつ説明します。
:has()
古いバージョンのChrome(v.101-104)を使用している場合は、Chromeフラグからこの機能を有効にできます。Chrome 101以降を使用していることを確認し、ブラウザのアドレスバーからchrome://flagsに移動します。
Experimental Web Platformの機能を[有効]に設定すると、準備が整います。:has()
ブラウザを再起動すると、ChromeブラウザでCSSを操作できます。
:has()
セレクターは何をしますか?:has()
疑似クラス、その使用法、およびプロパティを調べてみましょう。疑似クラスであるため、コロンを使用して任意のセレクターにアタッチでき、パラメーターとしてクラス、ID、およびHTMLタグを受け入れます。
以下のコードは、その一般的な使用法と構文を説明しています。クラスは、疑似クラス.selector
を使用してパラメーターとして渡された要素が含まれている場合にのみ選択されます。has()
.selector:has(.class) { ... }
.selector:has(#id) { ... }
.selector:has(div) { ... }
:has()
適切と思われる場合はいつでも、複数の疑似クラスを次々にチェーンできます。以下のコードは、この連鎖がどのように機能するかを示しています。
.selector:has(div):has(.class):has(#id) {
...
}
チェーンと同様に、複数の要素セレクターのリストを提供することもできますが、はるかに効率的です。
.selector:has(div, .class, #id) {
...
}
誤って無効な要素セレクターを:has()
疑似クラスに指定したとします。それを無視して、有効なセレクターのみを検討するのは十分にインテリジェントです。
.selector:has(div, accordion, .class, ::lobster, #id) {
...
}
accordion
および::lobster
は無効なセレクターであり、上記の場合は無視されます。開発者ツールにCSSエラーやアラートは表示されません。
:has()
CSSのセレクターが役立つシナリオの例をいくつか見てみましょう。
:has()
セレクターのデフォルトの動作は、特定の要素のセットが含まれている場合に何かを選択することであるため、これはセレクターの最も一般的な使用法である可能性があります。しかし、子要素のみを認識している場合はどうなるでしょうか。
ここでは、ユニバーサルセレクター(*
):has()
と子コンビネーター(>
)を使用して、親について何も知らなくてもすばやく親を選択できます。
上記のプロパティセグメントで説明したように、:has()
複数のエンティティのリストを渡すことができます。つまり、特定の要素内で必要な数のセレクターを確認できます。
:has()
前の兄弟の選択は、CSS隣接兄弟コンビネータを疑似クラスと組み合わせることによって可能になります。
ご存知かもしれませんが、隣接する兄弟コンビネータは、特定の要素の次の兄弟を選択します。この動作をで使用しhas()
て、前の兄弟を取得できます。簡単に言えば、要素に次の兄弟がある場合、コンビネータを使用して簡単に選択でき:has()
ます+
。
:has()
セレクターを使用すると、特定の子要素がある場合とない場合のスタイリングを回避できます。キャプションのある場合とない場合のFigure要素は、ここでの完璧な例です。
figcaption
要素はあるがテキストが含まれていない場合はどうなりますか?その場合、:not
と:empty
セレクターを使用してコンテンツをチェックできます。
図の装飾と同様に、複数の段落を持つブロック引用符のテキスト配置を簡単に切り替えることができます。ここで実際の動作を確認してください。
:empty
何も含まないスタイリング要素を要求する疑似クラスがすでにあります。空の状態をターゲットにする限り、期待どおりに機能しません。要素を空でないものとして認識するには、内部に1つのスペースで十分です。
この場合に使用するのは理想的ではありません:empty
。コンテンツのないカードもいくつか含まれるカードグリッドを作成しましょう。:has()
空のカードの状態をセレクターでスタイリングします。
記事のスタイルを設定する際、タイプ要素とブロック要素を揃えておくのは難しい作業です。コードブロック、図、およびブロック引用符を一般的な型要素と混合する例を考えてみましょう。
ブロック要素は、さまざまな活版印刷エンティティの中で目立つように、より垂直方向の間隔と装飾を持たせる必要があります。これを処理するためのCSSがいくつかあります。
p {
margin: 0;
}
p:not(:last-child) {
margin-bottom: 1.5em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.3;
margin: 1.5em 0 1.5rem;
color: #111;
}
pre,
figure,
blockquote {
margin: 3em 0;
}
figcaption {
margin-top: 1em;
font-size: 0.75em;
color: #888;
text-align: center;
}
上記のスニペットは、ここに示すように、見出しとブロック要素の間に不均一な垂直方向の間隔を生成します。しかし、以前の兄弟選択ハックを使用して、これらの不規則性を補うことは可能です。
CSSでCTAまたはボタンコンポーネントを2つのバリエーションで作成するとします。最初のコンポーネントはデフォルトのプレーンボタンで、2番目のコンポーネントはアイコン付きです。
:has()
これで、そのために2つの別々のクラスを作成することを計画している場合は、セレクターを使用して簡単に回避できます。キャッチは、単に子の.btn
要素をチェックし、.btn-icon
それに応じてスタイルを設定することです。
もちろん、ヘッダーコンポーネントに2つのレイアウトバリエーションがあるとします。1つは固定幅で、もう1つは液体です。
ヘッダーのコンテンツを固定幅内に維持するには、ヘッダー内にコンテンツラッパー要素を追加する必要があります。これら2つのヘッダーバージョンのマークアップの唯一の違いは、ラッパー要素です。
次に、以下のデモンストレーションに示すように、:has()
とセレクターをペアにして、CSSクラスを追加できます。:not()
レイアウトを調整する別の例は、グリッド内の列の数が特定の数に達したらすぐに変更することです。
minmax()
これは、列をグリッドに合わせるのに最適な幅を決定する関数を使用していない場合に便利です。
上記のように、グリッドは2つのアイテムのマークを超えるとすぐに、自動的に3列に調整されます。
対話性は、フィードバックに適切に接続されている場合に最もよく定義されます。インタラクティブなHTMLフォームに関しては、入力に関するフィードバックをユーザーに提供することが最善の方法です。
:has()
、、:valid
および疑似クラスの助けを借りて、:invalid
JavaScriptを使用せずに、フォームをより動的にすることができます。
上記の例は、:has()
セレクターのサポートが見つからない場合のエラーダイアログを示しています。これは@supports
、以下のコードスニペットに示すように、CSSルールを使用して実行できます。
@supports (selector(:has(*))) {
.selector:has(...) {
...
}
}
上記は、サポートを確認し、必要に応じて要素のスタイルを設定するための段階的な方法です。コードベースがいくつかの新機能を直接使用する場合、同じものの下位互換性のあるバージョンを作成する方法は次のとおりです。
@supports not (selector(:has(*))) {
.selector {
...
}
}
同じことを使用して、サポートが見つからなかった場合にユーザーにプロンプトを表示することもできます。
JavaScriptのSupportAPIを使用して、さまざまな機能のブラウザーサポートを検出することもできます。:has()
純粋にJavaScriptでサポートをチェックする例を次に示します。
if(!CSS.supports('selector(html:has(body))'))) { // if not supported
// Use the conventional JavaScript ways to emulate :has()
}
:has()
上記の記事では、レベル4のCSSセレクターについて学びました。そのニーズ、フロントエンドプロジェクトでJavaScriptを置き換えることができるいくつかのケース、および一般的なユースケースから高度なユースケースまでカバーしました。
また、さまざまなブラウザーによって提供されている現在のサポートについて説明し、ブラウザーのサポートを確認する方法を学びました。
ずっと読んでくれてありがとう。これを通して何か新しいことを学んだことを願っています。:has()
コメントでどのように役立つ かについて、より多くの例を自由に共有してください。
ソース:https ://blog.logrocket.com/advanced-guide-css-has-selector/
1659146400
Lớp giả CSS :has()
là một trong những tính năng được chờ đợi nhất trong nhiều năm. Đó là bộ chọn CSS cấp 4, hiện có sẵn dưới dạng tính năng được hỗ trợ đầy đủ trong Chrome 105 trở đi và có khả năng sẽ sớm trở thành một tính năng thông thường trên các trình duyệt khác.
Trong :has()
CSS là một lớp giả quan hệ cho phép bạn kiểm tra xem một phần tử đã cho có chứa các phần tử con nhất định hay không, chọn nó nếu tìm thấy bất kỳ kết quả phù hợp nào và sau đó tạo kiểu cho nó.
Bài viết này giải thích sự cần thiết của :has()
bộ chọn, cách sử dụng chung của nó, các ứng dụng và trường hợp sử dụng khác nhau từ đơn giản đến nâng cao, khả năng tương thích của trình duyệt và các dự phòng.
:has()
bộ chọn lại cần thiết?Hầu hết các nhà phát triển dựa vào JavaScript cho một số tính năng nhất định mà CSS không hỗ trợ theo mặc định. Tuy nhiên, các trình duyệt web ngày nay mạnh mẽ hơn bao giờ hết, điều này đang mở ra cánh cửa cho nhiều tính năng CSS mới và thú vị.
Bộ :has()
chọn là một trong những tính năng như vậy. Nó hoạt động trên phần tử gốc chứ không phải phần tử con và sử dụng danh sách các bộ chọn được phân tách bằng dấu phẩy làm đối số, sau đó tìm kiếm các kết quả phù hợp giữa các phần tử con của phần tử mà nó đại diện. Chức năng của nó tương tự như has()
phương thức jQuery.
Chúng ta hãy xem xét một số nhu cầu chung đối với :has()
bộ chọn trong phát triển giao diện người dùng để giải thích lý do tại sao tính năng này lại có nhu cầu cao như vậy trong nhiều năm.
Đầu tiên là khả năng kiểm tra xem một phần tử có chứa một số phần tử khác hay không, và sau đó tạo kiểu cho nó. JavaScript là cách duy nhất để làm điều này trước đây, như bạn có thể thấy ở đây:
let content = document.querySelector("#content"),
headings = []
if(content) {
headings = content.querySelectorAll("h1, h2, h3")
}
if(headings.length) {
// do something
}
Trong đoạn mã trên, chúng tôi đã sử dụng querySelectorAll
, đó là một phương pháp API Web để kiểm tra một phần tử phân chia cho các tiêu đề. Phương thức trả về một NodeList của các phần tử được cung cấp.
Một NodeList trống có nghĩa là không có phần tử nào hiện diện trong phần tử gốc. Có thể tìm thấy cách triển khai ở trên ở đây . Hoặc, bạn có thể xem CSS :has()
thay thế tại đây .
Thứ hai là khả năng chọn phần tử cha từ phần tử con. Một lần nữa, do thiếu cơ sở CSS để làm như vậy, các nhà phát triển đã dựa vào thuộc tính parentNode của Web API:
let el = document.querySelector(".someElement"),
elParent = null
if(el) {
elParent = el.parentNode
}
Xem bản demo CodePen của đoạn mã ở trên tại đây và cách :has()
bộ chọn thực hiện điều này tại đây .
Cuối cùng, có khả năng chọn anh chị em trước của một phần tử nhất định. Bộ chọn anh chị em trong CSS cho phép bạn chọn anh chị em kế tiếp, nhưng không có cách nào chỉ có CSS để chọn anh chị em trước đó. Việc triển khai JavaScript trông như thế này ( xem bản demo CodePen tại đây ):
let el = document.querySelector(".someElement"),
elPs = null
if(el) {
elPs = el.previousElementSibling
}
Bạn có thể xem cách thực hiện hành động này với bộ :has()
chọn tại đây .
Mọi thứ chúng ta đã làm ở trên với JavaScript bây giờ có thể đạt được với CSS :has()
. Chúng tôi sẽ trình bày từng cái một trong phần tiếp theo sau khi tìm hiểu cách bật và thử nghiệm tính năng này trên Google Chrome.
:has()
bộ chọnNếu bạn đang sử dụng phiên bản Chrome cũ hơn (v.101-104), bạn có thể bật tính năng này từ cờ Chrome. Đảm bảo bạn có Chrome 101+ và điều hướng đến chrome: // flags từ thanh địa chỉ của trình duyệt.
Đặt các tính năng Nền tảng web thử nghiệm thành Đã bật và bạn đã sẵn sàng. Khởi chạy lại trình duyệt và bạn có thể làm việc với CSS :has()
trong trình duyệt Chrome của mình.
:has()
làm gì?Hãy cùng khám phá lớp :has()
giả, cách sử dụng và thuộc tính của nó. Vì là lớp giả nên nó có thể được gắn vào bất kỳ bộ chọn nào bằng dấu hai chấm và chấp nhận các lớp, ID và thẻ HTML làm tham số.
Đoạn mã dưới đây giải thích cách sử dụng và cú pháp chung của nó. Lớp .selector
chỉ được chọn nếu nó chứa các phần tử được truyền cho nó dưới dạng tham số bằng cách sử dụng lớp has()
giả:
.selector:has(.class) { ... }
.selector:has(#id) { ... }
.selector:has(div) { ... }
Bạn có thể xâu chuỗi nhiều :has()
lớp giả lần lượt bất cứ khi nào bạn thấy phù hợp. Đoạn mã dưới đây trình bày cách hoạt động của chuỗi này:
.selector:has(div):has(.class):has(#id) {
...
}
Bạn cũng có thể cung cấp danh sách nhiều bộ chọn phần tử, tương tự như chuỗi, nhưng hiệu quả hơn nhiều:
.selector:has(div, .class, #id) {
...
}
Giả sử bạn đã vô tình cung cấp một bộ chọn phần tử không hợp lệ cho lớp :has()
giả. Đủ thông minh để bỏ qua điều đó và chỉ xem xét các bộ chọn hợp lệ:
.selector:has(div, accordion, .class, ::lobster, #id) {
...
}
accordion
và ::lobster
là các bộ chọn không hợp lệ và sẽ bị bỏ qua trong trường hợp trên. Bạn sẽ không thấy lỗi hoặc cảnh báo CSS nào trong các công cụ dành cho nhà phát triển.
Chúng ta hãy xem xét một số trường hợp ví dụ trong đó bộ chọn CSS :has()
có ích.
Đây có thể là cách sử dụng phổ biến nhất của :has()
bộ chọn, bởi vì hành vi mặc định của nó là chọn một cái gì đó nếu nó chứa một tập hợp các phần tử cụ thể. Nhưng điều gì sẽ xảy ra nếu chúng ta chỉ biết đến phần tử con?
*
Ở đây có thể sử dụng bộ chọn phổ quát ( ) với :has()
và bộ tổ hợp con ( >
) để nhanh chóng chọn bố mẹ mà không cần biết bất cứ điều gì về nó.
Như đã thảo luận trong phân đoạn thuộc tính ở trên, :has()
cho phép bạn chuyển một danh sách gồm nhiều thực thể, có nghĩa là bạn có thể kiểm tra bao nhiêu bộ chọn tùy thích trong một phần tử nhất định.
Việc lựa chọn anh chị em trước đó có thể thực hiện được bằng cách kết hợp bộ tổ hợp anh chị em liền kề CSS với lớp :has()
giả.
Như bạn có thể đã biết, bộ tổ hợp anh chị em kế cận sẽ chọn phần tử anh chị em tiếp theo của một phần tử nhất định. Chúng ta có thể sử dụng hành vi này với has()
để có được anh chị em trước đó. Nói một cách đơn giản, nếu một phần tử có anh chị em kế tiếp, thật dễ dàng để chọn nó với :has()
và +
tổ hợp!
Có thể tránh tạo kiểu riêng cho những thứ có và không có một số phần tử con nhất định bằng :has()
bộ chọn. Phần tử hình có và không có chú thích là ví dụ hoàn hảo ở đây.
Điều gì sẽ xảy ra nếu một figcaption
phần tử ở đó, nhưng không chứa bất kỳ văn bản nào? Trong trường hợp đó, chúng tôi có thể sử dụng bộ chọn :not
và :empty
để kiểm tra nội dung.
Tương tự như trang trí hình, chúng ta có thể dễ dàng chuyển đổi căn lề văn bản của các dấu ngoặc kép với nhiều hơn một đoạn văn. Xem nó hoạt động ở đây .
Chúng ta đã có một lớp giả được gọi :empty
cho các phần tử tạo kiểu không chứa gì. Liên quan đến việc nhắm mục tiêu trạng thái trống, nó không hoạt động như mong đợi. Một khoảng trống bên trong là đủ để nó nhận ra một phần tử là không trống.
Nó không lý tưởng để sử dụng :empty
trong trường hợp này. Hãy tạo một lưới thẻ cũng chứa một vài thẻ không có nội dung. Chúng tôi sẽ tạo kiểu các trạng thái thẻ trống bằng :has()
bộ chọn.
Trong khi tạo kiểu cho một bài báo, việc giữ cho các phần tử kiểu và khối được căn chỉnh là một công việc khó khăn. Hãy xem xét một ví dụ về trộn các khối mã, số liệu và dấu ngoặc kép với các phần tử kiểu chung.
Các phần tử khối nên có nhiều khoảng cách và trang trí theo chiều dọc hơn để nổi bật giữa các thực thể kiểu chữ khác nhau. Đây là một số CSS để xử lý điều đó.
p {
margin: 0;
}
p:not(:last-child) {
margin-bottom: 1.5em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.3;
margin: 1.5em 0 1.5rem;
color: #111;
}
pre,
figure,
blockquote {
margin: 3em 0;
}
figcaption {
margin-top: 1em;
font-size: 0.75em;
color: #888;
text-align: center;
}
Đoạn mã trên sẽ tạo ra khoảng cách theo chiều dọc không đồng đều giữa các tiêu đề và các phần tử khối, như được hiển thị ở đây. Nhưng có thể bù đắp cho những bất thường này bằng cách sử dụng hack lựa chọn anh chị em trước đó.
Giả sử bạn đang tạo CTA hoặc thành phần nút trong CSS với hai biến thể: biến thể đầu tiên là nút mặc định, đơn giản và biến thể thứ hai có biểu tượng.
Bây giờ, nếu bạn đang có kế hoạch viết hai lớp riêng biệt cho điều đó, bạn có thể dễ dàng tránh được điều đó với :has()
bộ chọn. Cách nắm bắt là chỉ cần kiểm tra .btn
phần tử cho một phần tử .btn-icon
con và sau đó định kiểu cho phù hợp.
Giả sử bạn có hai biến thể bố cục cho thành phần tiêu đề: một là chiều rộng cố định và một biến thể khác là chất lỏng, tất nhiên.
Để giữ cho nội dung của tiêu đề trong một chiều rộng cố định, chúng ta phải thêm một phần tử bao bọc nội dung bên trong nó. Sự khác biệt duy nhất giữa đánh dấu của hai phiên bản tiêu đề này là phần tử trình bao bọc.
Sau đó, chúng tôi có thể ghép nối :has()
với :not()
bộ chọn và thêm các lớp CSS, như được hiển thị trong minh họa bên dưới.
Một ví dụ khác về việc điều chỉnh bố cục có thể là sửa đổi số lượng cột trong lưới ngay khi chúng đạt đến một số lượng nhất định.
Điều này có thể hữu ích khi bạn không sử dụng minmax()
hàm xác định chiều rộng tốt nhất để vừa với một cột trong lưới của nó.
Như bạn có thể thấy ở trên, lưới sẽ tự động điều chỉnh thành ba cột ngay khi nó vượt quá mốc của hai mục.
Tương tác được xác định tốt nhất khi nó được kết nối đúng cách với phản hồi. Khi nói đến các biểu mẫu HTML tương tác, cung cấp phản hồi của người dùng về đầu vào của nó là điều tốt nhất bạn có thể làm.
Với sự trợ giúp của :has()
, :valid
và :invalid
các lớp giả, chúng ta có thể làm cho các biểu mẫu của mình năng động hơn rất nhiều và không cần đến JavaScript.
Các ví dụ được thảo luận ở trên hiển thị hộp thoại lỗi nếu không :has()
tìm thấy hỗ trợ cho bộ chọn. Điều này có thể được thực hiện bằng cách sử dụng @supports
quy tắc CSS, như được hiển thị trong các đoạn mã dưới đây:
@supports (selector(:has(*))) {
.selector:has(...) {
...
}
}
Trên đây là cách kiểm tra lũy tiến để hỗ trợ và tạo kiểu phần tử theo yêu cầu. Nếu một cơ sở mã sử dụng trực tiếp một số tính năng mới, thì đây là cách để viết các phiên bản tương thích ngược của giống nhau:
@supports not (selector(:has(*))) {
.selector {
...
}
}
Điều tương tự cũng có thể được sử dụng để nhắc người dùng nếu không tìm thấy hỗ trợ.
Bạn cũng có thể sử dụng API hỗ trợ trong JavaScript để phát hiện trình duyệt hỗ trợ các tính năng khác nhau. Đây là một ví dụ về việc kiểm tra :has()
hỗ trợ hoàn toàn bằng JavaScript:
if(!CSS.supports('selector(html:has(body))'))) { // if not supported
// Use the conventional JavaScript ways to emulate :has()
}
Trong bài viết trên, chúng ta đã tìm hiểu về :has()
bộ chọn CSS cấp 4. Chúng tôi đã đề cập đến các nhu cầu của nó, một số trường hợp mà nó có thể thay thế JavaScript trong các dự án giao diện người dùng của bạn và các trường hợp sử dụng chung đến nâng cao.
Chúng tôi cũng thảo luận về hỗ trợ hiện tại được cung cấp bởi các trình duyệt khác nhau và tìm hiểu cách kiểm tra hỗ trợ của trình duyệt.
Cảm ơn vì đã đọc tất cả các cách thông qua. Tôi hy vọng bạn đã học được điều gì đó mới thông qua điều này. Vui lòng chia sẻ thêm các ví dụ về cách :has()
có thể hữu ích trong các nhận xét.
Nguồn: https://blog.logrocket.com/advanced-guide-css-has-selector/