diff options
Diffstat (limited to 'docs/content/guide/components.md')
-rw-r--r-- | docs/content/guide/components.md | 348 |
1 files changed, 348 insertions, 0 deletions
diff --git a/docs/content/guide/components.md b/docs/content/guide/components.md new file mode 100644 index 0000000..ac8a77f --- /dev/null +++ b/docs/content/guide/components.md @@ -0,0 +1,348 @@ +--- +title: Components +description: Declaring and using components. +--- + +<Header title="Components"> +</Header> + +## Declaring and Using Components + +The components are simple text files that look like regular Jinja templates, with three requirements: + +**First**, components must be placed inside a folder registered in the catalog or a subfolder of it. + +```python +catalog.add_folder("myapp/components") +``` + +You can call that folder whatever you want, not just "components". You can also add more than one folder: + +```python +catalog.add_folder("myapp/layouts") +catalog.add_folder("myapp/components") +``` + +If you end up having more than one component with the same name, the one in the first folder will take priority. + +**Second**, they must have a ".jinja" extension. This also helps code editors automatically select the correct language syntax to highlight. However, you can configure it in the catalog. + +**Third**, the component name must start with an uppercase letter. Why? This is how JinjaX differentiates a component from a regular HTML tag when using it. I recommend using PascalCase names, like Python classes. + +The name of the file (minus the extension) is also how you call the component. For example, if the file is "components/PersonForm.jinja": + +``` +└ myapp/ + ├── app.py + ├── components/ + └─ PersonForm.jinja +``` + +The name of the component is "PersonForm" and can be called like this: + +From Python code or a non-component template: + +- `catalog.render("PersonForm")` + +From another component: + +- `<PersonForm> some content </PersonForm>`, or +- `<PersonForm />` + +If the component is in a subfolder, the name of that folder becomes part of its name too: + +``` +└ myapp/ + ├── app.py + ├── components/ + └─ person + └─ PersonForm.jinja +``` + +A "components/person/PersonForm.jinja" component is named "person.PersonForm", meaning the name of the subfolder and the name of the file separated by a dot. This is the full name you use to call it: + +From Python code or a non-component template: + +- `catalog.render("person.PersonForm")` + +From another component: + +- `<person.PersonForm> some content </person.PersonForm>`, or +- `<person.PersonForm />` + +Notice how the folder name doesn't need to start with an uppercase if you don't want it to. + +<a href="/static/img/anatomy-en.png" target="_blank"> + <img src="/static/img/anatomy-en.png" style="margin:0 auto;width:90%;max-width:35rem;"> +</a> + +## Taking Arguments + +More often than not, a component takes one or more arguments to render. Every argument must be declared at the beginning of the component with `{#def arg1, arg2, ... #}`. + +```html+jinja +{#def action, method="post", multipart=False #} + +<form method="{{ method }}" action="{{ action }}" + {%- if multipart %} enctype="multipart/form-data"{% endif %} +> + {{ content }} +</form> +``` + +In this example, the component takes three arguments: "action", "method", and "multipart". The last two have default values, so they are optional, but the first one doesn't. That means it must be passed a value when rendering the component. + +The syntax is exactly like how you declare the arguments of a Python function (in fact, it's parsed by the same code), so it can even include type comments, although they are not used by JinjaX (yet!). + +```python +{#def + data: dict[str, str], + method: str = "post", + multipart: bool = False +#} +... +``` + +## Passing Arguments + +There are two types of arguments: strings and expressions. + +### String + +Strings are passed like regular HTML attributes: + +```html+jinja +<Form action="/new" method="PATCH"> ... </Form> + +<Alert message="Profile updated" /> + +<Card title="Hello world" type="big"> ... </Card> +``` + +### Expressions + +There are two different but equivalent ways to pass non-string arguments: + +"Jinja-like", where you use double curly braces instead of quotes: + +```html+jinja title="Jinja-like" +<Example + columns={{ 2 }} + tabbed={{ False }} + panels={{ {'one': 'lorem', 'two': 'ipsum'} }} + class={{ 'bg-' + color }} +/> +``` + +... and "Vue-like", where you keep using quotes, but prefix the name of the attribute with a colon: + +```html+jinja title="Vue-like" +<Example + :columns="2" + :tabbed="False" + :panels="{'one': 'lorem', 'two': 'ipsum'}" + :class="'bg-' + color" +/> +``` + +<Callout type="note"> + For `True` values, you can just use the name, like in HTML: + <br> + ```html+jinja + <Example class="green" hidden /> + ``` +</Callout> + +<Callout type="note"> + You can also use dashes when passing an argument, but they will be translated to underscores: + <br> + ```html+jinja + <Example aria-label="Hi" /> + ``` + <br> + ```html+jinja title="Example.jinja" + {#def aria_label = "" #} + ... + ``` +</Callout> + +## With Content + +There is always an extra implicit argument: **the content** inside the component. Read more about it in the [next](/guide/slots) section. + +## Extra Arguments + +If you pass arguments not declared in a component, those are not discarded but rather collected in an `attrs` object. + +You then call `attrs.render()` to render the received arguments as HTML attributes. + +For example, this component: + +```html+jinja title="Card.jinja" +{#def title #} +<div {{ attrs.render() }}> + <h1>{{ title }}</h1> + {{ content }} +</div> +``` + +Called as: + +```html +<Card title="Products" class="mb-10" open>bla</Card> +``` + +Will be rendered as: + +```html +<div class="mb-10" open> + <h1>Products</h1> + bla +</div> +``` + +You can add or remove arguments before rendering them using the other methods of the `attrs` object. For example: + +```html+jinja +{#def title #} +{% do attrs.set(id="mycard") -%} + +<div {{ attrs.render() }}> + <h1>{{ title }}</h1> + {{ content }} +</div> +``` + +Or directly in the `attrs.render()` call: + +```html+jinja +{#def title #} + +<div {{ attrs.render(id="mycard") }}> + <h1>{{ title }}</h1> + {{ content }} +</div> +``` + +<Callout type="info"> +The string values passed into components as attrs are not cast to `str` until the string representation is **actually** needed, for example when `attrs.render()` is invoked. +</Callout> + +### `attrs` Methods + +#### `.render(name=value, ...)` + +Renders the attributes and properties as a string. + +Any arguments you use with this function are merged with the existing +attibutes/properties by the same rules as the `HTMLAttrs.set()` function: + +- Pass a name and a value to set an attribute (e.g. `type="text"`) +- Use `True` as a value to set a property (e.g. `disabled`) +- Use `False` to remove an attribute or property +- The existing attribute/property is overwritten **except** if it is `class`. + The new classes are appended to the old ones instead of replacing them. +- The underscores in the names will be translated automatically to dashes, + so `aria_selected` becomes the attribute `aria-selected`. + +To provide consistent output, the attributes and properties +are sorted by name and rendered like this: +`<sorted attributes> + <sorted properties>`. + +```html+jinja +<Example class="ipsum" width="42" data-good /> +``` +```html+jinja +<div {{ attrs.render() }}> +<!-- <div class="ipsum" width="42" data-good> --> + +<div {{ attrs.render(class="abc", data_good=False, tabindex=0) }}> +<!-- <div class="abc ipsum" width="42" tabindex="0"> --> +``` + +<Callout type="warning"> +Using `<Component {{ attrs.render() }}>` to pass the extra arguments to other components **WILL NOT WORK**. That is because the components are translated to macros before the page render. + +You must pass them as the special argument `_attrs`. + +```html+jinja +{#--- WRONG 😵 ---#} +<MyButton {{ attrs.render() }} /> + +{#--- GOOD 👍 ---#} +<MyButton _attrs={{ attrs }} /> +<MyButton :_attrs="attrs" /> +``` +</Callout> + +#### `.set(name=value, ...)` + +Sets an attribute or property + +- Pass a name and a value to set an attribute (e.g. `type="text"`) +- Use `True` as a value to set a property (e.g. `disabled`) +- Use `False` to remove an attribute or property +- If the attribute is "class", the new classes are appended to + the old ones (if not repeated) instead of replacing them. +- The underscores in the names will be translated automatically to dashes, + so `aria_selected` becomes the attribute `aria-selected`. + +```html+jinja title="Adding attributes/properties" +{% do attrs.set( + id="loremipsum", + disabled=True, + data_test="foobar", + class="m-2 p-4", +) %} +``` + +```html+jinja title="Removing attributes/properties" +{% do attrs.set( + title=False, + disabled=False, + data_test=False, + class=False, +) %} +``` + +#### `.setdefault(name=value, ...)` + +Adds an attribute, but only if it's not already present. + +The underscores in the names will be translated automatically to dashes, so `aria_selected` +becomes the attribute `aria-selected`. + +```html+jinja +{% do attrs.setdefault( + aria_label="Products" +) %} +``` + +#### `.add_class(name1, name2, ...)` + +Adds one or more classes to the list of classes, if not already present. + +```html+jinja +{% do attrs.add_class("hidden") %} +{% do attrs.add_class("active", "animated") %} +``` + +#### `.remove_class(name1, name2, ...)` + +Removes one or more classes from the list of classes. + +```html+jinja +{% do attrs.remove_class("hidden") %} +{% do attrs.remove_class("active", "animated") %} +``` + +#### `.get(name, default=None)` + +Returns the value of the attribute or property, +or the default value if it doesn't exist. + +```html+jinja +{%- set role = attrs.get("role", "tab") %} +``` + +...
\ No newline at end of file |