limenius / liform Goto Github PK
View Code? Open in Web Editor NEWPHP library to render Symfony Forms to JSON Schema
License: MIT License
PHP library to render Symfony Forms to JSON Schema
License: MIT License
Been trying for the longest time to solve this without any luck, so I'm writing this here as a last stand.
The following is one of the first controllers I've made, which is where i would be using Liform
class CreateController extends AbstractController
{
public function __construct(private readonly CategoryProjector $categoryProjector, private readonly Liform $liform)
{
}
#[Route('/api/management/categories/create', name: 'management.categories.create', methods:['get', 'post'])]
public function __invoke(Request $request, EntityManagerInterface $entityManager): Response
{
$categoryModel = new CategoryModel();
$form = $this->createForm(CategoryCreateType::class, $categoryModel, );
if ($form->isSubmitted() && $form->isValid()) {
$event = new CategoryCreated($categoryModel);
$this->categoryProjector->ApplyCategoryCreated($event);
return new Response(Response::HTTP_OK);
}
if ($request->isMethod('GET')) {
return new Response(json_encode($this->liform->transform($form)));
}
return new Response(Response::HTTP_BAD_REQUEST);
}
}
But no matter what i try, it always comes back to the same error Undefined array key "compound" when i use $liform->transform.
I then went to the line where the issue happened and for some reason the types are not what I at least expected them to be.
So just for context, this is the function in resolver where it breaks. specifically here 'transformer' => $this->transformers['compound']['transformer'], which when looking at what it receives makes sense, if it's supposed to be an array of the different types anyway.
public function resolve(FormInterface $form)
{
$types = FormUtil::typeAncestry($form);
foreach ($types as $type) {
if (isset($this->transformers[$type])) {
return $this->transformers[$type];
}
}
// Perhaps a compound we don't have a specific transformer for
if (FormUtil::isCompound($form)) {
return [
'transformer' => $this->transformers['compound']['transformer'],
'widget' => null,
];
}
throw new TransformerException(
sprintf(
'Could not find a transformer for any of these types (%s)',
implode(', ', $types)
)
);
}
for further context, this is the Form which i am trying to transform.
class CategoryCreateType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('type', TextType::class, [
'required' => false,
'attr' => [
'placeholder' => 'Category Type',
]
]);
$builder
->add('name', TextType::class, [
'required' => true,
]);
$builder
->add('description', TextType::class, [
'required' => false,
]);
$builder
->add('image', FileType::class, [
'required' => true,
]);
}
}
But for some reason in Resolver.php, the $types array gets the following, instead of the actual types in the form.
[0]: 'category_create',
[1]: 'form'
If i am lucky enough to have anyone read this that might have an idea as to what could be the issue, please let me know, i am utterly lost.
$transformerData['transformer'] is an instance of AbstractTransfomer, and the isRequired method is protected , I also don't find any hack way, why does this line works? this is quite strange.
We can't override the fields property "propertyOrder" returned by liform.
Currently this is what I use, it works good if you don't fail the fields order set. But my quick solution has multiple default, like more than one field can have the same position, don't have a lot of time to make a correct solution.
<?php
namespace AppBundle\Liform\Transformer;
use Symfony\Component\Form\FormInterface;
use Limenius\Liform\Transformer\CompoundTransformer as BaseCompoundTransformer;
class CompoundTransformer extends BaseCompoundTransformer
{
/** {@inheritdoc} */
public function transform(FormInterface $form, array $extensions = [], $widget = null)
{
$data = [];
$order = 1;
$required = [];
foreach ($form->all() as $name => $field) {
// ...
if ($field->getConfig()->getOption('liform')
&& isset($field->getConfig()->getOption('liform')['propertyOrder'])
) {
$transformedChild['propertyOrder'] = $field->getConfig()->getOption('liform')['propertyOrder'];
} else {
$transformedChild['propertyOrder'] = $order;
$order ++;
}
// ...
}
// ...
}
}
Happens in Symfony5:
Error: Call to undefined method Chess\WebBundle\Translation\Translator::transChoice()
#10 /vendor/limenius/liform/src/Limenius/Liform/Serializer/Normalizer/FormErrorNormalizer.php(99): Limenius\Liform\Serializer\Normalizer\FormErrorNormalizer::getErrorMessage
#9 /vendor/limenius/liform/src/Limenius/Liform/Serializer/Normalizer/FormErrorNormalizer.php(70): Limenius\Liform\Serializer\Normalizer\FormErrorNormalizer::convertFormToArray
#8 /vendor/limenius/liform/src/Limenius/Liform/Serializer/Normalizer/FormErrorNormalizer.php(47): Limenius\Liform\Serializer\Normalizer\FormErrorNormalizer::normalize
Hi, I am trying to implement a collection type field, where I can have multiple rows and each one has fields.
But, unfortunatly the collection type just give one row, not all on related rows.
This is what i get from output:
{
"schema": {
"title": "visaexpress_wizardbundle_wizard",
"type": "object",
"properties": {
"wizard_questions": {
"type": "array",
"title": "wizard_questions",
"items": {
"title": "0",
"type": "object",
"properties": {
"question": {
"type": "string",
"title": "question",
"propertyOrder": 1
},
"helper": {
"type": "string",
"title": "helper",
"widget": "textarea",
"propertyOrder": 2
}
},
"required": [
"question"
]
},
"propertyOrder": 1
}
},
"required": [
"wizard_questions"
]
}
}
It should render 5 rows, but I just get one.
Anyone knows what might be ?
Thanks in advance.
Hi!
I am probably not doing properly the thing as it should be done, but I prefer to ask.
I am simply trying to get the json structure of a simple form as following:
$builder
->add('name', TextType::class)
->add('owner', EntityType::class, [
'class' => User::class, 'choice_label' => 'email'
])
->add('coop', EntityType::class, [
'class' => Coop::class, 'choice_label' => 'name'
])
;
$restaurant = new Restaurant();
$resolver = new Resolver();
$liform = new Liform($resolver);
$form = $this->createForm(RestaurantType::class, $restaurant, ['csrf_protection' => false]);
$schema = json_encode($liform->transform($form));
die(var_dump($schema));
From the doc, I really don't understand the part where the setTransformer is done on the resolver. Does this part is mandatory? How this part as to be done properly?
Thank you in advance.
I was wondering if they are supported, or planned to be developed: http://json-schema.org/understanding-json-schema/reference/conditionals.html
Have you any plan to update your library and made it compatible with symfony/form ^4.4 | ^5.0 ?
I'll plan to use your library but cannot install it because of symfony/form version.
Currently to output a 'description' it required setting the 'description' key within the 'liform' form option, however I think it would be good if it would use the help key (from Symfony 4.1) and then override with the liform description value if it's set? (Similarly to the widget override).
I'm happy to do a PR + tests for this.
Currently relies on a global version
From the discussion in #2 it is clear that we should provide more docs about this. Apart from just the example in the sandbox. Also work more on documenting transformers.
I got the following error when normalizing a form containing SubmitButton
with the InitialValuesNormalizer
:
Argument 1 passed to Limenius\Liform\Serializer\Normalizer\InitialValuesNormalizer::getValues() must be an instance of Symfony\Component\Form\Form, instance of Symfony\Component\Form\SubmitButton given, called in my-project/vendor/limenius/liform/src/Limenius/Liform/Serializer/Normalizer/InitialValuesNormalizer.php on line 62
The getValues()
function requires a Form
but the SubmitButton
is not one but implements FormInterface
.
In my case, replacing Form
by FormInterface
does the job but may require further testing.
This is the list of things that I would consider a must for v1.0
We can extract much information from validators. See this example of minLength and this code for guessing.
Note that this code, that is used in Symfony, will only extract maxLength if Symfony is forces to guess the type of the validator. (using null
as type in the builder). This would probably be a good PR to Symfony to fix this issue. So I guess the example of minLength is better than the example of maxLength.
JSON Schema keywords:
We could add our own validation keywords, but I think that this would be the minimum. It can also happen that some of these keywords don't make sense or cannot be extracted.
Other:
Document how to upload files. We already have an example in the Sandbox using Base64 for file uploading, but a doc section will prevent issues and questions.
Check i18n. I have not been paying much attention to i18n. It shouldn't be difficult to fix issues, but this needs to be checked.
Hiya,
i was wondering if this is still in use by limenius and is this still under maintaince?
Would be nice to know if the symfony bundle will be support flex / symfony 4?
Cheers,
Toni
Symfony 6 would be great. Related to #56 , since it requires php8.
During the implementation of LiformBundle in my project (Symfony 4), I noticed that some transformers are missing. In particular, the transformers for hidden
and submit
inputs are missing and I got error about that.
This is how my form type looks:
$builder
->setMethod('GET')
->add('param1', TextType::class)
->add('param2', HiddenType::class)
->add('search', SubmitType::class);
Creating new transformers has solved the problem so I wonder if we can do the same in Liform library...or maybe there is some other solution?
Thanks!
Hi there
I've been attempting to implement Liform (through the Symfony Liform Bundle) together with the liform-react library. So far I've had good success and really appreciate your sharing this with the public.
I have, however, run into one issue:
The "required" attribute of the ChoiceTransformer (AbstractTransformer) is not passed correctly to the liform-react ChoiceWidget (or so I believe). Here's my best explanation of what is happening:
This is the json output of Liform:
{
"announcementSchema":{
"title":"updaterbundle_announcement",
"type":"object",
"properties":{
"announcement":{
"type":"string",
"title":"announcement",
"attr":{
"id":"updater-textarea",
"placeholder":"Enter line update ..."
},
"default":"Enter line update ...",
"widget":"textarea",
"propertyOrder":1
},
"announcementType":{
"enum":[
"One",
"Two",
"Three"
],
"liform":{
"enum_titles":[
"0",
"1",
"2"
]
},
"type":"string",
"title":"Category",
"attr":{
"placeholder":"-- Choose --"
},
"default":"-- Choose --",
"propertyOrder":2
}
},
"required":[
"announcement",
"announcementType"
]
},
"announcementInitialValues":{
},
"baseUrl":"\/updater\/",
"location":"\/updater\/"
}
Everything looks correct according to Liform documentation. The two fields "announcement" and "announcementType" are contained in the "required" array.
Here is a snippet from renderField.js within liform-react that makes use of that Liform generated schema:
var isRequired = exports.isRequired = function isRequired(schema, fieldName) {
if (!schema.required) {
return false;
}
return schema.required.indexOf(fieldName) != 1;
};
The problem is that the "schema" parameter passed in to that javascript function does not contain the "required" parameter since it is for an individual field (announcementType) and not the entire object. Here is what the "schema" parameter holds:
attr: Object
placeholder: "-- Choose --"
default: "-- Choose --"
enum: Array[3]
0: "One"
1: "Two"
2: "Three"
length: 3
liform: Object
enum_titles: Array[3]
0: "0"
1: "1"
2: "2"
length: 3
propertyOrder: 2
title: "Category"
type: "string"
A possible fix would be to include an "addRequired" function to the Liform AbstractTransformer that looks something like this:
/**
* @param mixed $form
* @param array $schema
*
* @return array
*/
protected function addRequired($form, $schema)
{
// If it has already been set then don't override it
if (!isset($schema['required']) && $required = $form->getConfig()->getOption('required')) {
$schema['required'] = [$form->getName()];
}
return $schema;
}
And we'd add that into the addCommonSpecs function of course:
/**
* @param mixed $form
* @param array $schema
* @param array $extensions
* @param string $widget
*
* @return array
*/
protected function addCommonSpecs($form, $schema, $extensions = [], $widget)
{
$schema = $this->addLabel($form, $schema);
$schema = $this->addAttr($form, $schema);
$schema = $this->addPattern($form, $schema);
$schema = $this->addDefault($form, $schema);
$schema = $this->addDescription($form, $schema);
$schema = $this->addRequired($form, $schema);
$schema = $this->addWidget($form, $schema, $widget);
$schema = $this->applyExtensions($extensions, $form, $schema);
return $schema;
}
Does this make sense? Or is there a better way of solving this issue?
Kyle
There is this little piece of code that allows us to modify the schema per type, kind of what buildView
does in Sf Forms.
This should be documented.
the errors normalizer create an object like this :
{
code : "string value or null",
children : { field1 : { errors: [ "error string value for field 1" ] } , fieldX : { children : { fieldY : { errors : [ " errors string value for fieldY" ] } } } },
message: "validation failed or something else ..." // string type
}
the format expected for redux form via submission errors should be like this
{
field1: ["error string value for field 1"],
fieldX: { fieldY: ["error string value for field Y"] }
}
i'll personally add a extra value in normalizer to get errors object who fit for redux form.
i already created a function to convert errors created from normalizer to a compatible errors object for redux form this is the function for those who want try it directly for javascript side.
the data from my ajax function look like
{ schema :{}, initialsValues:{}, errors:{} }
it's why i'm looking for, and using data.errors.errors
submitFunction() {
// note the return, throw a error if the fetch function isnt returned while using throw new SubmissionError(errorsObject)
return yourFetchFunction().then(
data => {
if(typeof data.errors !== 'undefined'){ // checking if ajax sending errors values in data results
let errors = {}
let convertToReduxFormErrors = (obj) => {
let objectWithoutChildrenAndFalseErrors = {}
Object.keys(obj).map( (name) => {
if(name === 'children'){
objectWithoutChildrenAndFalseErrors = { ...objectWithoutChildrenAndFalseErrors, ...convertToReduxFormErrors(obj[name])}
}else{
if(typeof obj[name]['children'] !== 'undefined'){
objectWithoutChildrenAndFalseErrors[name] = convertToReduxFormErrors(obj[name])
}else{
if(typeof obj[name]['errors'] !== 'undefined' && !_.isEmpty(obj[name]['errors']) ){ // using lodash for empty error check, dont add them if empty
objectWithoutChildrenAndFalseErrors[name] = obj[name]['errors']
}
}
}
}
)
return objectWithoutChildrenAndFalseErrors
}
let ObjErrors = convertToReduxFormErrors(data.errors.errors)
errors = { _error: data.errors.message, ...ObjErrors }
throw new SubmissionError( errors )
}
}
)
}
render() {
return (
<Liform schema={this.state.schema} initialValues={this.state.initialValues} onSubmit={ this.submitFunction.bind(this)}/>
);
}
the doc say that error normalizer should be handled by liform-react, but i dont saw any way to directly send normalizer error data to liform Component.
https://limenius.github.io/liform-react/#/liform-react/examples/validation
you can see this exemple dont show how to use the normalizer
submission error doc from redux form
https://redux-form.com/7.1.2/docs/api/submissionerror.md/
this code is for test purpose, when using redux, ajax request have to be handled by async action
https://www.andrewhfarmer.com/react-ajax-best-practices/
https://redux.js.org/docs/advanced/AsyncActions.html
when i'll finish to add my own normalizer handling for addind this purpose, i'll make a PR if this is confirmed as a bug. i'll add additional properties to errors object for prevent BC breaks
added PR for this issue : #20
if PR is merged you could be able to do this after :
yourFetch().then(
data => {
if(typeof data.errors !== 'undefined'){ // checking if ajax sending errors values in data results
throw new SubmissionError( { ...data.errors.redux_form_errors } )
}
}
)
note that the parameter "format" in "normalize" function look like unused, i think we may use it to set only redux_form_errors or normal errors normalizing or both in returning normalizer array, but i didnt used it since you may want use it for other purpose
From README.md
:
If the attribute placeholder of attr is provided [...] It will be extracted as the default option.
According to html5 spec[0], placeholders are meant as hints for the input, and may contain a non-valid value
The placeholder attribute represents a short hint (a word or short phrase) intended to aid the user with data entry. A hint could be a sample value or a brief description of the expected format
I propose removing the feature of using placeholder attribute for a default value.
I have my constraints defined directly in my form and not in the entity.
Would it be possible to add the constraints from the form if there is no constraints on the entity?
No updates in 16 months. I need this for a project running on PHP8 but can't cause all the PRs are in limbo
Hi,
When we have a choiceType, the attribute 'choices' is an array with key
and value
.
When the form is serialized, each key
goes in the enum_titles
but they are not translated.
I think that the expected behaviour is to have these keys translated, as this is the default behaviour in the createView
method of Symfony.
Do you know if this would be possible ?
Liform serializes CheckboxType type to 1/0 instead of true/false. Then when liform-react attempts to validate it, it fails because it wants a true or false. I found that making the following change within Serializer/Normalizer/FormViewNormalizer.php fixes it:
From:
} else {
return $object->vars['value'];
}
To:
} else {
if (isset($object->vars['checked'])) {
return $object->vars['checked'];
}
return $object->vars['value'];
}
Is this the correct place to be fixing the error?
Can Liform serialize Symfony forms to json comptible with Json Schema Form (https://github.com/json-schema-form)?
I've submitted some PRs to answear's fork of this library: https://github.com/answear/Liform/pulls
I also have my own fork of answear's liform-bundle, which of course needs liform, but didn't work with the latest Symfony dependency injection. I don't care which version of Liform the bundle uses, as long as it's current and works with my bundle without bombarding me with deprecation messages.
My request is that this repo and https://github.com/answear/Liform unify so that developers don't need to choose which library to use.
Perhaps a new release that drops unsupported dependencies? PHP 7 is dead, Symfony < 5.4 is dead (and 5.4 will be EOL very shortly).
I'm happy to contribute PRs to support that, but I'm not sure to which repo I should be submitting them.
This is a really cool library, I struggled to find PHP code to generate a schema that followed existing standards. Creating the schema from a form is a clever idea, and this implementation looks like it will work very well for my application, so thanks again for releasing it.
DateType which uses choice
widget type (3 selects - year, month and day) has wrong schema when transformed by liform.
Adding type:
$builder->add('dateOfBirth', DateType::class, [
'widget' => 'choice',
'placeholder' => [
'year' => 'Year',
'month' => 'Month',
'day' => 'Day'
],
'format' => 'yyyyMMdd',
'translation_domain' => 'booking-form'
]);
What I expect in json schema:
"dateOfBirth": {
"type": "object",
"title": "dateOfBirth",
"properties": {
"year": {
"type": "number",
"title": "year",
"propertyOrder": 1
},
"month": {
"type": "number",
"title": "month",
"propertyOrder": 2
},
"day": {
"type": "number",
"title": "day",
"propertyOrder": 3
},
},
"propertyOrder": 1
}
What We've got currently which is wrong imo:
"dateOfBirth": {
"type": "string",
"title": "dateOfBirth",
"propertyOrder": 1
}
transformers are not clear in the documentation and i don't know what i should do to create a new one for FileType to handle image uploads
It seems that currently the Labels are not translated. A Field has a TranslationDomain in symfony that can be used to overwrite the default NULL (which resolves to "messages").
http://symfony.com/doc/current/reference/forms/types/form.html#translation-domain
Can be integrated the same way the Error Translation is done, in file:
https://github.com/Limenius/Liform/blob/master/src/Limenius/Liform/Transformer/AbstractTransformer.php
It might also be useful for [liform][description].
Hello,
Is there a way to specify serialization groups before serialization of a form ?
I've registered my own normalizers for several entities and unfortunately, I have to specify a serialization group to retrieve any specific set of fields and it seems to limit what I retrieve using liform.
Thanks
Got this error when I tried to generate a schema with liform :
CRITICAL - Uncaught PHP Exception Symfony\Component\Debug\Exception\UndefinedMethodException: "Attempted to call an undefined method named "getBlockPrefix" of class "Symfony\Component\Form\Extension\DataCollector\Proxy\ResolvedTypeDataCollectorProxy"." at /home/admin/Documents/displayce/code/vendor/limenius/liform/src/Limenius/Liform/FormUtil.php line 39
here my code :
$resolver = new Resolver();
//$resolver->setDefaultTransformers();
$liform = new Liform($resolver);
$editForm = $this->createForm(new EditType(), $entity, array( 'method' => 'PUT', 'csrf_protection' => false, ));
$schema = json_encode($this->get('liform')->transform($editForm));
I saw a bug related to that and it seems to be fixed with 2.8 version but of course my project is using 2.7 : https://github.com/symfony/symfony/issues/15760
My form contains RepeatedType
of PasswordType
:
$builder
->add('plainPassword', RepeatedType::class, [
'type' => PasswordType::class,
])
;
validation rules looks like this:
plainPassword:
- NotBlank: ~
- Length:
min: 6
max: 255
so ValidatorGuesser
can't guess length and StringTransformer::addMinLength()
doesn't add minLength
.
I would suggest to look at attr.minlength
option like in addMaxLength()
method and, if it not exists, run default algorithm of guessing by validation rules.
$translationDomain = $form->getConfig()->getOption('translation_domain');
$translationParameters = $form->getConfig()->getOption('help_translation_parameters');
if ($help = $formConfig->getOption('help', '')) {
$schema['description'] = $this->translator->trans($help,$translationParameters,$translationDomain );
}
@nacmartin I've found that the following annotation is not being serialized as expected:
Using the Length annotation from the Symfony's doc, it turns out that minLength is correctly being added but not maxLength.
I noticed that you implemented a new ValidatorGuesser for covering the missing guessMinLength
in ValidatorTypeGuesser from the Symfony core.
I also found that you're using your custom class to guess the minLength based on some constraints but you're not doing the same for the other methods, eg: addMaxLength.
So, using something like:
/**
* @Assert\NotBlank()
* @Assert\Length(
* min = 2,
* max = 50,
* minMessage = "Your first name must be at least {{ limit }} characters long",
* maxMessage = "Your first name cannot be longer than {{ limit }} characters"
* )
*
* @ORM\Column(type="string", length=50)
*/
private $name;
we will end up receiving the following json-schema:
name: {type: "string", title: "Name", minLength: 2, propertyOrder: 1}
minLength: 2
propertyOrder: 1
title: "Name"
type: "string"
as you can see the maxLength is missing.
For now, I figured out that I can still fix this by adding the maxLength option attr into my form field configuration but I would definitely wish to rely on my entity constraints.
Is there any way to solve this?
This Package requires Php7.2.5, so when I tried to upgrade a project which uses this package it gave me an error.
Symfony 2.8
I have a form:
$builder->add('name', TextType::class);
$builder->add('contacts', CollectionType::class, [...]);
The normalizer produces this output:
stdClass Object
(
[name] => Test
[contacts] => stdClass Object
(
[0] => stdClass Object
(
[value] => TestValue
)
)
)
instead of:
stdClass Object
(
[name] => Test
[contacts] => Array <--------- array
(
[0] => stdClass Object
(
[value] => TestValue
)
)
)
I'm passing this data to a JS in this way inside <script>
in .twig
:
window.form.initialValues = {{ initialValues|json_encode|raw }}
stdClass and Array are different in JSON format. stdClass gives {0: item}
instead of [item]
.
As a result I have error while using liform-react
in ConnectedFieldArray.js:83
nextValue.every is not a function
When I hardcoded StdClass -> Array conversion, all works
$initialValues->contacts = json_decode(json_encode($initialValues->contacts), true);
According RFC Section minItems
validation keyword for array
should be set at the same level as items
keyword, not inside the last one.
You use a dedicated normalizer InitialValuesNormalizer
to extract default values from the FormView class. then, I guess the frontend web developer will fill these default values to the form later. it may be a lot easier if the default value is in the form schema. I can see that the ChoiceTransformer saves choices data into schema, is it proper to do the same with the default value like following JSON object?
{
"title":null,
"properties":{
"name":{
"type":"string",
"title":"Name",
"propertyOrder":1,
"defaultValue": "john"
}
}
}
There is a snipped code in ChoiceTransformer
class, do you define these structure by specification or just make up by yourself? In my case, I need to write a AutocompleteType which has serval configurations, such as route, route parameters, autocomplete_alias, etc, these information is delivered to client side, I am wondering the proper place they should be.
$schema = [
'items' => [
'type' => 'string',
'enum' => $choices,
'enum_titles' => $titles,
'minItems' => $this->isRequired($form) ? 1 : 0,
],
'uniqueItems' => true,
'type' => 'array',
];
In this function, the guesser is optional but the resolver is required.
I think we should reverse the order of the parameters.
public function __construct(TranslatorInterface $translator,
FormTypeGuesserInterface $validatorGuesser = null,
ResolverInterface $resolver)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.