Skip to content

Commit b066280

Browse files
committed
feature #1826 Introduce cookbook documentation (WebMamba)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- Introduce cookbook documentation | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Issues | | License | MIT This PR introduces a new section for the UX website: cookbook. This section aims to give more concrete documentation, show examples of common things implemented with UX components, and go deeper into some concepts that we do in the documentation. The goal is not to replace the documentation but to illustrate the documentation with concrete examples. <img width="724" alt="Capture d’écran 2024-05-04 à 17 14 24" src="https://github.com/symfony/ux/assets/32077734/25e5279e-c4d3-4f80-9534-2bbbcc91ee8a"> <img width="1438" alt="Capture d’écran 2024-05-04 à 17 15 38" src="https://github.com/symfony/ux/assets/32077734/28bd2569-2dde-4bbc-9a2e-3905a1b9ad90"> ## How it's implemented? Simply by creating a .md file in the recipes directory at the route of the project, then everything is generated automatically. There are cookbook pages that show all the recipes available, then you can click on the recipe to read it. ## Who can add recipes to the cookbook? Anyone! If you thing you implemented something a bit complex but common, just show you did it! Thank you! Tell me what you think 😁 Commits ------- 6bfa523 Introduce cookbook documentation
2 parents c1befad + 6bfa523 commit b066280

File tree

17 files changed

+607
-2
lines changed

17 files changed

+607
-2
lines changed
Loading

ux.symfony.com/assets/styles/app.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
@import "components/ProductGrid";
8383
@import "components/PackageHeader";
8484
@import "components/PackageBox";
85+
@import "components/Cookbook";
8586
@import "components/Tabs";
8687
@import "components/Tag";
8788
@import "components/Terminal";
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
.Cookbook {
2+
h1 {
3+
margin-top: 3rem;
4+
margin-bottom: 1rem;
5+
text-align: center;
6+
font-size: 52px;
7+
font-weight: 700;
8+
line-height: 60px;
9+
}
10+
11+
.description {
12+
text-align: center;
13+
font-size: 24px;
14+
font-weight: 600;
15+
margin-top: 1.5rem;
16+
}
17+
18+
.tags {
19+
display: flex;
20+
justify-content: center;
21+
align-items: center;
22+
width: 100%;
23+
gap: 1rem;
24+
text-decoration: none;
25+
list-style: none;
26+
margin-bottom: 3rem;
27+
28+
li {
29+
background-color: rgb(74 29 150);
30+
color: rgb(202 191 253);
31+
font-weight: 500;
32+
font-size: 0.75rem;
33+
line-height: 1rem;
34+
padding: .125rem .625rem;
35+
border-radius: 0.25rem;
36+
}
37+
}
38+
39+
.image-title {
40+
width: 100%;
41+
max-height: 40vh;
42+
overflow: hidden;
43+
border-radius: 4px;
44+
margin-bottom: 3rem;
45+
46+
img {
47+
display: block;
48+
object-fit: contain;
49+
width: 100%;
50+
}
51+
}
52+
53+
.content {
54+
h2 {
55+
margin-top: 3rem;
56+
margin-bottom: 1rem;
57+
font-size: 32px;
58+
font-weight: 700;
59+
line-height: 40px;
60+
}
61+
62+
h3 {
63+
margin-top: 3rem;
64+
margin-bottom: 1rem;
65+
font-size: 24px;
66+
font-weight: 700;
67+
line-height: 32px;
68+
color: #FFFFFF;
69+
}
70+
}
71+
72+
pre {
73+
margin-top: 4rem;
74+
margin-bottom: 2rem;
75+
border-radius: 4px;
76+
background-color: #0A0A0A;
77+
padding: 2rem;
78+
}
79+
}

ux.symfony.com/composer.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ux.symfony.com/config/services.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ services:
1010
_defaults:
1111
autowire: true # Automatically injects dependencies in your services.
1212
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
13+
bind:
14+
string $cookbookPath: '%kernel.project_dir%/cookbook'
1315

1416
# makes classes in src/ available to be used as services
1517
# this creates a service per class whose id is the fully-qualified class name
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
---
2+
title: Architecture component
3+
description: Rules and pattern to work with components
4+
image: images/cookbook/component_architecture.png
5+
tags:
6+
- javascript
7+
- symfony
8+
---
9+
10+
## Introduction
11+
12+
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).
13+
Those two packages allow you to create reusable components in your Symfony application.
14+
But the component architecture is not exclusive to Symfony, it is a design pattern that can be applied to any programming language or framework.
15+
And the js world already implement this architecture for long time, on many different frameworks like React, Vue, or Svelte.
16+
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.
17+
So let's see what are those rules!
18+
19+
## 4 Rules
20+
21+
### Composition
22+
23+
A page is no longer just a page, but rather a collection of small, reusable components.
24+
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.
25+
The training list component could even be composed of smaller components, such as a training card component.
26+
The goal is to create the most atomic, and reusable components possible.
27+
28+
#### How does it work into Symfony?
29+
30+
In Symfony you can have a component Alert for example with the following template:
31+
32+
```twig
33+
<div class="alert alert-{{ type }}">
34+
<twig:Icon name="{{ icon }}" />
35+
{{ message }}
36+
</div>
37+
```
38+
39+
So here you can see we have an alert component that his himself use an Icon component.
40+
Or you can make composition with the following syntax:
41+
42+
```twig
43+
<twig:Card>
44+
<twig:CardHeader>
45+
<h2>My Card</h2>
46+
</twig:CardHeader>
47+
<twig:CardBody>
48+
<p>This is the content of my card.</p>
49+
</twig:CardBody>
50+
</twig:Card>
51+
```
52+
53+
So here we Card component, and we give to the content of this component mutliple other components.
54+
55+
### Independence
56+
57+
This is a really important rule, and not obvious. But your component should leave on his own context,
58+
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.
59+
This rule make your component trully reusable.
60+
61+
***How does it work into Symfony?***
62+
63+
Symfony keep the context of the page into the context of your component. So this your own responsability to follow this rules.
64+
But notice that if there are conflic between a variable from the context page and your component, your component context override the page context.
65+
66+
### Props
67+
68+
Our component must remain independent, but we can customize it props.
69+
Let's take the example of a button component. You have your component that look on every page the same,
70+
the only change is the label. What you can do is to declare a prop `label` into your button component.
71+
And so now when you want to use your button component, you can pass the label you want as props. The component gonna take
72+
this props at his initialization and keep it all his life long.
73+
74+
***How does it work into Symfony?***
75+
76+
Let's take the example of the Alert component an [anonymous component](https://symfony.com/bundles/ux-twig-component/current/index.html#anonymous-components).
77+
We have the following template:
78+
79+
```twig
80+
{% props type, icon, message %}
81+
82+
<div class="alert alert-{{ type }}">
83+
<twig:Icon name="{{ icon }}" />
84+
{{ message }}
85+
</div>
86+
```
87+
88+
Just like that we define three props for our Alert component. And know we can use like this:
89+
90+
```twig
91+
<twig:Alert type="success" icon="check" message="Your account has been created." />
92+
```
93+
94+
If your component anonymous but a class component, you can simply define props
95+
by adding property to your class.
96+
97+
```php
98+
class Alert
99+
{
100+
public string $type;
101+
public string $icon;
102+
public string $message;
103+
}
104+
```
105+
106+
There are something really important to notice with props. It's your props
107+
should only go into one direction from the parent to child. But your props should never
108+
go up. **If your child need to change something in the parent, you should use events**.
109+
110+
### State
111+
112+
A state is pretty much like a prop but the main difference is a state can
113+
change during the life of the component. Let's take the example of a button component.
114+
You can have a state `loading` that can be `true` or `false`. When the button is clicked
115+
the state `loading` can be set to `true` and the button can display a loader instead of the label.
116+
And when the loading is done, the state `loading` can be set to `false` and the button can display the label again.
117+
118+
***How does it work into Symfony?***
119+
120+
In symfony you 2 different approach to handle state. The first one is to use stimulus directly
121+
in to your component. What we recommend to do is to set a controller stimulus at the root of your component.
122+
123+
```twig
124+
{% props label %}
125+
126+
<button data-controller="button" data-button-label="{{ label }}">
127+
{{ label }}
128+
</button>
129+
```
130+
131+
And then you can define your controller like this:
132+
133+
```js
134+
import { Controller } from 'stimulus';
135+
136+
export default class extends Controller {
137+
static values = { label: String };
138+
139+
connect() {
140+
this.element.textContent = this.labelValue;
141+
}
142+
143+
loading() {
144+
this.element.textContent = 'Loading...';
145+
}
146+
}
147+
```
148+
149+
The second approach is to use the [LiveComponent](https://symfony.com/bundles/ux-live-component/current/index.html) package.
150+
How to choose between the two? If your component don't need any backend logic
151+
for his state keep it simple and use stimulus approach. But if you need to handle
152+
backend logic for your state, use LiveComponent.
153+
With live component a live prop is a state. So if you want store the number of click on a button you can do
154+
the following component:
155+
156+
```php
157+
<?php
158+
159+
#[AsLiveComponent]
160+
class Button
161+
{
162+
#[LiveProp]
163+
public int $clicks = 0;
164+
165+
public function increment()
166+
{
167+
$this->clicks++;
168+
169+
$this->save();
170+
}
171+
}
172+
```
173+
174+
## Conclusion
175+
176+
Even in Symfony, you can use the component architecture.
177+
Follow those rules help your front developpers working on codebase
178+
their are familiar with since those rules are already used in the js world.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace App\Controller;
13+
14+
use App\Service\CookbookFactory;
15+
use App\Service\CookbookRepository;
16+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
17+
use Symfony\Component\HttpFoundation\Response;
18+
use Symfony\Component\Routing\Attribute\Route;
19+
20+
class CookbookController extends AbstractController
21+
{
22+
public function __construct(
23+
private CookbookRepository $cookbookRepository,
24+
private CookbookFactory $cookbookFactory,
25+
) {
26+
}
27+
28+
#[Route('/cookbook', name: 'app_cookbook_index')]
29+
public function index(): Response
30+
{
31+
$cookbooks = $this->cookbookRepository->findAll();
32+
33+
return $this->render('cookbook/index.html.twig', [
34+
'cookbooks' => $cookbooks,
35+
]);
36+
}
37+
38+
#[Route('/cookbook/{slug}', name: 'app_cookbook_show')]
39+
public function show(string $slug): Response
40+
{
41+
$cookbook = $this->cookbookRepository->findOneByName($slug);
42+
43+
return $this->render('cookbook/show.html.twig', [
44+
'slug' => $slug,
45+
'cookbook' => $cookbook,
46+
]);
47+
}
48+
}

ux.symfony.com/src/Model/Cookbook.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace App\Model;
13+
14+
class Cookbook
15+
{
16+
public function __construct(
17+
public string $title,
18+
public string $description,
19+
public string $route,
20+
public string $image,
21+
public string $content,
22+
/**
23+
* @var string[]
24+
*/
25+
public array $tags = [],
26+
) {
27+
}
28+
}

ux.symfony.com/src/Service/CommonMark/ConverterFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use League\CommonMark\CommonMarkConverter;
1515
use League\CommonMark\Extension\ExternalLink\ExternalLinkExtension;
16+
use League\CommonMark\Extension\FrontMatter\FrontMatterExtension;
1617
use League\CommonMark\Extension\Mention\MentionExtension;
1718
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
1819
use Tempest\Highlight\CommonMark\HighlightExtension;
@@ -47,6 +48,7 @@ public function __invoke(): CommonMarkConverter
4748
->addExtension(new ExternalLinkExtension())
4849
->addExtension(new MentionExtension())
4950
->addExtension(new HighlightExtension())
51+
->addExtension(new FrontMatterExtension())
5052
;
5153

5254
return $converter;

0 commit comments

Comments
 (0)