元件開發:
tailwindcss + alpinejs + laravel component
<div class="author-bio">
<img class="author-bio__image" src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
<div class="author-bio__content">
<h2 class="author-bio__name">Adam Wathan</h2>
<p class="author-bio__body">
Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
</p>
</div>
</div>
“Best practices”
“Best practices” don’t actually work.
- Dealing with similar components
- What if we needed to add a new type of content that also needed the same styling?
Utility-Fiirst
<div class="flex">
<div class="flex-none w-48 relative">
<img src="/classic-utility-jacket.jpg" alt="" class="absolute inset-0 w-full h-full object-cover" />
</div>
<form class="flex-auto p-6">
<div class="flex flex-wrap">
<h1 class="flex-auto text-xl font-semibold">
Classic Utility Jacket
</h1>
<div class="text-xl font-semibold text-gray-500">
$110.00
</div>
<div class="w-full flex-none text-sm font-medium text-gray-500 mt-2">
In stock
</div>
</div>
<div class="flex items-baseline mt-4 mb-6">
<div class="space-x-2 flex">
<label>
<input class="w-9 h-9 flex items-center justify-center bg-gray-100 rounded-lg" name="size" type="radio" value="xs" checked>
XS
</label>
<label>
<input class="w-9 h-9 flex items-center justify-center" name="size" type="radio" value="s">
S
</label>
<label>
<input class="w-9 h-9 flex items-center justify-center" name="size" type="radio" value="m">
M
</label>
<label>
<input class="w-9 h-9 flex items-center justify-center" name="size" type="radio" value="l">
L
</label>
<label>
<input class="w-9 h-9 flex items-center justify-center" name="size" type="radio" value="xl">
XL
</label>
</div>
<div class="ml-auto text-sm text-gray-500 underline">Size Guide</div>
</div>
<div class="flex space-x-3 mb-4 text-sm font-medium">
<div class="flex-auto flex space-x-3">
<button class="w-1/2 flex items-center justify-center rounded-md bg-black text-white" type="submit">Buy now</button>
<button class="w-1/2 flex items-center justify-center rounded-md border border-gray-300" type="button">Add to bag</button>
</div>
<button class="flex-none flex items-center justify-center w-9 h-9 rounded-md text-gray-400 border border-gray-300" type="button" aria-label="like">
<svg width="20" height="20" fill="currentColor">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" />
</svg>
</button>
</div>
<p class="text-sm text-gray-500">
Free shipping on all continental US orders.
</p>
</form>
</div>
Bootstrap Utilities
https://getbootstrap.com/docs/4.6/utilities/spacing/
Reusability?
- React component
- Vue component
- Laravel Blade:
@include('user/profile-box') - Tailwind's @apply directive:
compiled css class
.btn {
@apply text-base font-medium rounded-lg p-3;
}
.btn--primary {
@apply bg-rose-500 text-white;
}
.btn--secondary {
@apply bg-gray-100 text-black;
}
Why front-end frameworks?
- business state <-> UI state
- all fine when it's 1-to-1 relationship
- jQuery + native JS is enough
{
items: [
{
name: 'product 1',
price: 100,
},
{
name: 'product 2',
price: 300,
},
]
sum: 400
}
<div>
<div>Product 1: $100</div>
<div>Product 2: $300</div>
<hr />
<div>You total cost: $400</div>
</div>
When it's not 1-to-1 but 1-to-N
class TodoApp extends React.Component {
constructor(props) {
super(props);
this.state = { items: [], text: '' };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
render() {
return (
<div>
<h3>TODO</h3>
<TodoList items={this.state.items} />
<form onSubmit={this.handleSubmit}>
<label htmlFor="new-todo">
What needs to be done?
</label>
<input
id="new-todo"
onChange={this.handleChange}
value={this.state.text}
/>
<button>
Add #{this.state.items.length + 1}
</button>
</form>
</div>
);
}
handleChange(e) {
this.setState({ text: e.target.value });
}
handleSubmit(e) {
e.preventDefault();
if (this.state.text.length === 0) {
return;
}
const newItem = {
text: this.state.text,
id: Date.now()
};
this.setState(state => ({
items: state.items.concat(newItem),
text: ''
}));
}
}
class TodoList extends React.Component {
render() {
return (
<ul>
{this.props.items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
);
}
}
ReactDOM.render(
<TodoApp />,
document.getElementById('todos-example')
);
One way data flow
Modern front-end framework?
- one way data flow
- update state from children or sibling
- trigger event (abstract behavior in 2.)
programming: it's all about state & behavior
Alpinejs
<script src="//unpkg.com/alpinejs" defer></script>
<div x-data="{ open: false }">
<button @click="open = true">Expand</button>
<span x-show="open">
Content...
</span>
</div>
- no need to run npm
- no need to run webpack
- no `npm run dev` or `npm run prod`
- BACK-END developers' DREAM
Events?
HTML Custom Events
Laravel Components
php artisan make:component Alert
App\View\Components\Alert.php
resources/views/components/alert.blade.php
<x-alert/>
<x-alert>
<x-slot name="title">
Server Error
</x-slot>
<strong>Whoops!</strong> Something went wrong!
</x-alert>
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class Alert extends Component
{
public $type;
public $message;
public function __construct($type, $message)
{
$this->type = $type;
$this->message = $message;
}
public function render()
{
return view('components.alert');
}
}
- when @include('user/profile')
is not enough - when you want to extract
pure UI
Real world example: modal
<h2>Basic Modal</h2>
<div class="mt-4">
<button
x-data
@click="$dispatch('basic-modal', { show: true })"
type="button"
class="leading-tight bg-blue-600 text-white rounded px-6 py-3 text-sm focus:outline-none focus:border-white"
>
Show Modal
</button>
</div>
<x-modal :id="'basic-modal'">
<x-slot name="title">
<h5 class="text-xl font-bold">Title of the modal</h5>
</x-slot>
<x-slot name="content">
<p>You are watching this text in tailwind css model with alpine JS.</p>
</x-slot>
<x-slot name="footer">
<button
@click="show = false"
type="button"
class="bg-gray-700 text-gray-100 rounded px-4 py-2 mr-1"
>
Close
</button>
<button
type="button"
class="bg-blue-600 text-gray-200 rounded px-4 py-2"
>
Saves Changes
</button>
</x-slot>
</x-modal>
<hr class="my-4" />
<h2>Link Modal</h2>
<div class="mt-4">
Click
<a
x-data
@click="$dispatch('link-modal', { show: true })"
class="text-blue-500 cursor-pointer hover:underline"
>this link</a>
to open modal.
</div>
<x-modal :id="'link-modal'">
<x-slot name="title">
<h5 class="text-xl font-bold">Link modal</h5>
</x-slot>
<x-slot name="content">
<p>You are watching this text in tailwind css model with alpine JS.</p>
</x-slot>
<x-slot name="footer">
<button
@click="show = false"
type="button"
class="bg-gray-700 text-gray-100 rounded px-4 py-2"
>
Close
</button>
</x-slot>
</x-modal>
<hr class="my-4" />
<h2>Long Content Modal</h2>
<div class="mt-4">
<button
x-data
@click="$dispatch('long-modal', { show: true })"
type="button"
class="leading-tight bg-blue-600 text-white rounded px-6 py-3 text-sm focus:outline-none focus:border-white"
>
Click Me
</button>
</div>
<x-modal :id="'long-modal'">
<x-slot name="title">
<h5 class="text-xl font-bold">Long Content Modal</h5>
</x-slot>
<x-slot name="content">
<p>
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
</p>
</x-slot>
<x-slot name="footer">
<button
@click="show = false"
type="button"
class="bg-gray-700 text-gray-100 rounded px-4 py-2"
>
Close
</button>
</x-slot>
</x-modal>
<hr class="my-4" />
<h2>Long Title Modal</h2>
<div class="mt-4">
<button
x-data
@click="$dispatch('long-title-modal', { show: true })"
type="button"
class="leading-tight bg-blue-600 text-white rounded px-6 py-3 text-sm focus:outline-none focus:border-white"
>
Show Modal
</button>
</div>
<x-modal :id="'long-title-modal'">
<x-slot name="title">
<h5 class="text-xl font-bold">This is the title of a very long title modal. Test if it is it is it is it is it is it is overlapping with the cross icon.</h5>
</x-slot>
<x-slot name="content">
<p>You are watching this text in tailwind css model with alpine JS.</p>
</x-slot>
<x-slot name="footer">
<button
@click="show = false"
type="button"
class="bg-gray-700 text-gray-100 rounded px-4 py-2 mr-1"
>
Close
</button>
<button
type="button"
class="bg-blue-600 text-gray-200 rounded px-4 py-2"
>
Saves Changes
</button>
</x-slot>
</x-modal>
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class Modal extends Component
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return view('components.modal');
}
}
<div
x-data="{ show: false }"
x-on:{{ $id }}.window="show = $event.detail.show"
@keydown.escape.window="show = false"
id="{{ $id }}"
x-init="$watch('show', value => document.body.classList.toggle('overflow-hidden', value))"
>
<div
x-show="show"
style="display: none; background-color: rgba(0,0,0,0.5);"
tabindex="0"
class="z-40 overflow-auto left-0 top-0 bottom-0 right-0 w-full h-full fixed"
>
<div
@click.away="show = false"
class="z-50 relative p-3 mx-auto my-0 max-w-full"
style="width: 600px;"
>
<div class="bg-white rounded shadow-lg border flex flex-col overflow-hidden">
<button
@click="show = false"
class="fill-current h-6 w-6 absolute right-0 top-0 m-6 font-3xl font-bold"
>
×
</button>
<div class="pl-4 pr-10 py-3 border-b">
{{ $title }}
</div>
<div class="p-4 flex-grow">
{{ $content }}
</div>
<div class="px-4 py-4 border-t">
<div class="flex justify-end">
{{ $footer }}
</div>
</div>
</div>
</div>
</div>
</div>
- Didn't compile anything!
- tailwindcss + alpinejs + laravel component = works well~!
- backend developers will like it!
- 1 full-stack dev + N backend devs = very productive!
Conclusion 1
- Build your own bootstrap
- NOT very confident for:
SPA / highly interactive applications
Conclusion 2
- Be Critical:
Don't believe in major opinions easily - Focus on fundamentals:
Portable skills - Be bold:
So you can be creative
Thanks.
Tailwind
By howtomakeaturn
Tailwind
- 886