Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce cookbook documentation #1826

Merged
merged 1 commit into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions ux.symfony.com/assets/styles/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
@import "components/ProductGrid";
@import "components/PackageHeader";
@import "components/PackageBox";
@import "components/Cookbook";
@import "components/Tabs";
@import "components/Tag";
@import "components/Terminal";
Expand Down
79 changes: 79 additions & 0 deletions ux.symfony.com/assets/styles/components/_Cookbook.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
.Cookbook {
h1 {
margin-top: 3rem;
margin-bottom: 1rem;
text-align: center;
font-size: 52px;
font-weight: 700;
line-height: 60px;
}

.description {
text-align: center;
font-size: 24px;
font-weight: 600;
margin-top: 1.5rem;
}

.tags {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
gap: 1rem;
text-decoration: none;
list-style: none;
margin-bottom: 3rem;

li {
background-color: rgb(74 29 150);
color: rgb(202 191 253);
font-weight: 500;
font-size: 0.75rem;
line-height: 1rem;
padding: .125rem .625rem;
border-radius: 0.25rem;
}
}

.image-title {
width: 100%;
max-height: 40vh;
overflow: hidden;
border-radius: 4px;
margin-bottom: 3rem;

img {
display: block;
object-fit: contain;
width: 100%;
}
}

.content {
h2 {
margin-top: 3rem;
margin-bottom: 1rem;
font-size: 32px;
font-weight: 700;
line-height: 40px;
}

h3 {
margin-top: 3rem;
margin-bottom: 1rem;
font-size: 24px;
font-weight: 700;
line-height: 32px;
color: #FFFFFF;
}
}

pre {
margin-top: 4rem;
margin-bottom: 2rem;
border-radius: 4px;
background-color: #0A0A0A;
padding: 2rem;
}
}
2 changes: 1 addition & 1 deletion ux.symfony.com/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions ux.symfony.com/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ services:
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
bind:
string $cookbookPath: '%kernel.project_dir%/cookbook'

# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
Expand Down
178 changes: 178 additions & 0 deletions ux.symfony.com/cookbook/architecture_component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
---
title: Architecture component
description: Rules and pattern to work with components
image: images/cookbook/component_architecture.png
tags:
- javascript
- symfony
---

## Introduction

In SymfonyUX exist two packages: [TwigComponents](https://symfony.com/bundles/ux-twig-component/current/index.html) and [LiveComponent](https://symfony.com/bundles/ux-live-component/current/index.html).
Those two packages allow you to create reusable components in your Symfony application.
But the component architecture is not exclusive to Symfony, it is a design pattern that can be applied to any programming language or framework.
And the js world already implement this architecture for long time, on many different frameworks like React, Vue, or Svelte.
So, a set of rules and pattern has already be defined to work with components. This is why SymfonyUX try to be as close as possible to those rules.
So let's see what are those rules!

## 4 Rules

### Composition

A page is no longer just a page, but rather a collection of small, reusable components.
These components can be assembled to form a page. For example, there could be a component for the title and another for the training list.
The training list component could even be composed of smaller components, such as a training card component.
The goal is to create the most atomic, and reusable components possible.

#### How does it work into Symfony?

In Symfony you can have a component Alert for example with the following template:

```twig
<div class="alert alert-{{ type }}">
<twig:Icon name="{{ icon }}" />
{{ message }}
</div>
```

So here you can see we have an alert component that his himself use an Icon component.
Or you can make composition with the following syntax:

```twig
<twig:Card>
<twig:CardHeader>
<h2>My Card</h2>
</twig:CardHeader>
<twig:CardBody>
<p>This is the content of my card.</p>
</twig:CardBody>
</twig:Card>
```

So here we Card component, and we give to the content of this component mutliple other components.

### Independence

This is a really important rule, and not obvious. But your component should leave on his own context,
he should not be aware of the rest of the page. You should to talk one component into a page, to another and it should work exactly the same.
This rule make your component trully reusable.

***How does it work into Symfony?***

Symfony keep the context of the page into the context of your component. So this your own responsability to follow this rules.
But notice that if there are conflic between a variable from the context page and your component, your component context override the page context.

### Props

Our component must remain independent, but we can customize it props.
Let's take the example of a button component. You have your component that look on every page the same,
the only change is the label. What you can do is to declare a prop `label` into your button component.
And so now when you want to use your button component, you can pass the label you want as props. The component gonna take
this props at his initialization and keep it all his life long.

***How does it work into Symfony?***

Let's take the example of the Alert component an [anonymous component](https://symfony.com/bundles/ux-twig-component/current/index.html#anonymous-components).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we find a system for generic local links ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get what you mean. Do you implements something to generate better link to the docs ?

We have the following template:

```twig
{% props type, icon, message %}

<div class="alert alert-{{ type }}">
<twig:Icon name="{{ icon }}" />
{{ message }}
</div>
```

Just like that we define three props for our Alert component. And know we can use like this:

```twig
<twig:Alert type="success" icon="check" message="Your account has been created." />
```

If your component anonymous but a class component, you can simply define props
by adding property to your class.

```php
class Alert
{
public string $type;
public string $icon;
public string $message;
}
```

There are something really important to notice with props. It's your props
should only go into one direction from the parent to child. But your props should never
go up. **If your child need to change something in the parent, you should use events**.

### State

A state is pretty much like a prop but the main difference is a state can
change during the life of the component. Let's take the example of a button component.
You can have a state `loading` that can be `true` or `false`. When the button is clicked
the state `loading` can be set to `true` and the button can display a loader instead of the label.
And when the loading is done, the state `loading` can be set to `false` and the button can display the label again.

***How does it work into Symfony?***

In symfony you 2 different approach to handle state. The first one is to use stimulus directly
in to your component. What we recommend to do is to set a controller stimulus at the root of your component.

```twig
{% props label %}

<button data-controller="button" data-button-label="{{ label }}">
{{ label }}
</button>
```

And then you can define your controller like this:

```js
import { Controller } from 'stimulus';

export default class extends Controller {
static values = { label: String };

connect() {
this.element.textContent = this.labelValue;
}

loading() {
this.element.textContent = 'Loading...';
}
}
```

The second approach is to use the [LiveComponent](https://symfony.com/bundles/ux-live-component/current/index.html) package.
How to choose between the two? If your component don't need any backend logic
for his state keep it simple and use stimulus approach. But if you need to handle
backend logic for your state, use LiveComponent.
With live component a live prop is a state. So if you want store the number of click on a button you can do
the following component:

```php
<?php

#[AsLiveComponent]
class Button
{
#[LiveProp]
public int $clicks = 0;

public function increment()
{
$this->clicks++;

$this->save();
}
}
```

## Conclusion

Even in Symfony, you can use the component architecture.
Follow those rules help your front developpers working on codebase
their are familiar with since those rules are already used in the js world.
48 changes: 48 additions & 0 deletions ux.symfony.com/src/Controller/CookbookController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace App\Controller;

use App\Service\CookbookFactory;
use App\Service\CookbookRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class CookbookController extends AbstractController
{
public function __construct(
private CookbookRepository $cookbookRepository,
private CookbookFactory $cookbookFactory,
) {
}

#[Route('/cookbook', name: 'app_cookbook_index')]
public function index(): Response
{
$cookbooks = $this->cookbookRepository->findAll();

return $this->render('cookbook/index.html.twig', [
'cookbooks' => $cookbooks,
]);
}

#[Route('/cookbook/{slug}', name: 'app_cookbook_show')]
public function show(string $slug): Response
{
$cookbook = $this->cookbookRepository->findOneByName($slug);

return $this->render('cookbook/show.html.twig', [
'slug' => $slug,
'cookbook' => $cookbook,
]);
}
}
28 changes: 28 additions & 0 deletions ux.symfony.com/src/Model/Cookbook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace App\Model;

class Cookbook
{
public function __construct(
public string $title,
public string $description,
public string $route,
public string $image,
public string $content,
/**
* @var string[]
*/
public array $tags = [],
) {
}
}
2 changes: 2 additions & 0 deletions ux.symfony.com/src/Service/CommonMark/ConverterFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Extension\ExternalLink\ExternalLinkExtension;
use League\CommonMark\Extension\FrontMatter\FrontMatterExtension;
use League\CommonMark\Extension\Mention\MentionExtension;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Tempest\Highlight\CommonMark\HighlightExtension;
Expand Down Expand Up @@ -47,6 +48,7 @@ public function __invoke(): CommonMarkConverter
->addExtension(new ExternalLinkExtension())
->addExtension(new MentionExtension())
->addExtension(new HighlightExtension())
->addExtension(new FrontMatterExtension())
;

return $converter;
Expand Down
Loading